Aura Design System

Card

Displays a card with header, content, and footer sections.

Preview

Loading...
import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
  CardFooter,
  CardAction,
} from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { AspectRatio } from "@/components/ui/AspectRatio";

export function CardDemo() {
  return (
  <Card className="max-w-sm">
    <AspectRatio ratio={16 / 9}>
      <img
        src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80"
        alt="Photo by Drew Beamer"
        className="size-full object-cover"
      />
    </AspectRatio>
    <CardHeader>
      <CardTitle>Beautiful Landscape</CardTitle>
      <CardDescription>Captured in the mountains</CardDescription>
    </CardHeader>
    <CardContent>
      <p className="small text-gray-12">
        A stunning view of nature's beauty captured at golden hour.
      </p>
    </CardContent>
    <CardFooter>
      <Button>View Details</Button>
    </CardFooter>
  </Card>
)

Installation

Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces

pnpm dlx shadcn@latest add @aura/card

Manual

Copy and paste the class names utility into your utils/class-names.ts file.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Copy and paste the Card component into your components/ui/Card.tsx file.

/**
 * @description Displays a card with header, content, and footer sections.
 * CardAction is the right-hand slot of CardHeader (top right).
 */
import * as React from "react";

import { cn } from "@/utils/class-names";

type CardProps = React.ComponentProps<"div"> & {
  /**
   * Clickable card. Hover uses a gray-8 border and a soft shadow.
   * The card is in the tab order and shows a focus-visible ring.
   */
  interactive?: boolean;
};

function Card({
  className,
  interactive = false,
  tabIndex,
  onKeyDown,
  onClick,
  ...props
}: CardProps) {
  const clickable = interactive || typeof onClick === "function";

  return (
    <div
      data-slot="card"
      data-interactive={clickable ? "true" : undefined}
      tabIndex={clickable ? (tabIndex ?? 0) : tabIndex}
      onClick={onClick}
      onKeyDown={
        clickable
          ? (event) => {
              onKeyDown?.(event);
              if (event.defaultPrevented) return;
              if (event.key === "Enter" || event.key === " ") {
                event.preventDefault();
                event.currentTarget.click();
              }
            }
          : onKeyDown
      }
      className={cn(
        "overflow-hidden border border-gray-6 bg-gray-2 rounded-(--radius-md)",
        clickable &&
          "cursor-pointer transition-colors motion-reduce:transition-none hover:border-gray-8 hover:shadow-[0_2px_8px_var(--gray-a3)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-8",
        className
      )}
      {...props}
    />
  );
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-header"
      className={cn(
        "grid auto-rows-min items-start gap-x-1 gap-y-0.5 p-1 pb-0 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
        className
      )}
      {...props}
    />
  );
}

function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
  return (
    <h3
      data-slot="card-title"
      className={cn("small m-0 font-semibold text-gray-12", className)}
      {...props}
    />
  );
}

function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="card-description"
      className={cn("small m-0 text-gray-11", className)}
      {...props}
    />
  );
}

/**
 * Right-hand slot of CardHeader. Place it as a direct child so it sits at the top right, beside the title and description.
 */
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-action"
      className={cn(
        "col-start-2 row-span-2 row-start-1 flex items-center gap-0.5 self-start justify-self-end",
        className
      )}
      {...props}
    />
  );
}

function CardContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-content"
      className={cn("small p-1 pt-0 text-gray-12", className)}
      {...props}
    />
  );
}

function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-footer"
      className={cn(
        "flex items-center gap-0.5 border-t border-gray-6 p-1",
        className
      )}
      {...props}
    />
  );
}

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

Anatomy

CardAction is the right-hand slot of CardHeader. Place it as a direct child and it sits at the top right, beside the title and description.

Usage

WithFooter

export const WithFooter = () => (
  <Card>
    <CardHeader>
      <CardTitle>Project Setup</CardTitle>
      <CardDescription>Configure your project settings</CardDescription>
    </CardHeader>
    <CardContent>
      <p className="small text-gray-12">
        Your project has been created successfully.
      </p>
    </CardContent>
    <CardFooter>
      <Button>Continue</Button>
    </CardFooter>
  </Card>
)

WithActions

export const WithActions = () => (
  <Card>
    <CardHeader>
      <CardTitle>Notifications</CardTitle>
      <CardDescription>You have 3 unread messages</CardDescription>
      <CardAction>
        <Button variant="menu">Mark all read</Button>
      </CardAction>
    </CardHeader>
    <CardContent>
      <div className="space-y-1">
        <p className="small text-gray-12">New comment on your post</p>
        <p className="small text-gray-11">2 minutes ago</p>
      </div>
    </CardContent>
  </Card>
)

SimpleCard

export const SimpleCard = () => (
  <Card>
    <CardContent>
      <p className="small text-gray-12">A simple card with just content.</p>
    </CardContent>
  </Card>
)

MultipleCards

export const MultipleCards = () => (
  <div className="flex flex-col gap-1">
    <Card>
      <CardHeader>
        <CardTitle>First Card</CardTitle>
        <CardDescription>This is the first card</CardDescription>
      </CardHeader>
      <CardContent>
        <p className="small text-gray-12">Content for the first card.</p>
      </CardContent>
    </Card>
    <Card>
      <CardHeader>
        <CardTitle>Second Card</CardTitle>
        <CardDescription>This is the second card</CardDescription>
      </CardHeader>
      <CardContent>
        <p className="small text-gray-12">Content for the second card.</p>
      </CardContent>
    </Card>
    <Card>
      <CardHeader>
        <CardTitle>Third Card</CardTitle>
        <CardDescription>This is the third card</CardDescription>
      </CardHeader>
      <CardContent>
        <p className="small text-gray-12">Content for the third card.</p>
      </CardContent>
    </Card>
  </div>
)

NestedCards

export const NestedCards = () => (
  <Card>
    <CardHeader>
      <CardTitle>Parent Card</CardTitle>
      <CardDescription>This card contains nested cards</CardDescription>
    </CardHeader>
    <CardContent>
      <div className="flex flex-col gap-1">
        <Card className="bg-gray-3">
          <CardContent>
            <p className="small text-gray-12">Nested card 1</p>
          </CardContent>
        </Card>
        <Card className="bg-gray-3">
          <CardContent>
            <p className="small text-gray-12">Nested card 2</p>
          </CardContent>
        </Card>
      </div>
    </CardContent>
  </Card>
)

InteractiveCard

export const InteractiveCard = () => (
  <Card interactive>
    <CardHeader>
      <CardTitle>Clickable Card</CardTitle>
      <CardDescription>Hover to see the effect</CardDescription>
    </CardHeader>
    <CardContent>
      <p className="small text-gray-12">
        This card has hover states for interactive experiences.
      </p>
    </CardContent>
  </Card>
)

CardGrid

export const CardGrid = () => (
  <div className="grid grid-cols-1 gap-1 sm:grid-cols-2 md:grid-cols-3">
    {Array.from({ length: 6 }).map((_, i) => (
      <Card key={i}>
        <CardHeader>
          <CardTitle>Card {i + 1}</CardTitle>
        </CardHeader>
        <CardContent>
          <p className="small text-gray-12">Grid item content</p>
        </CardContent>
      </Card>
    ))}
  </div>
)

ImageGrid

export const ImageGrid = () => (
  <div className="grid grid-cols-1 gap-1 sm:grid-cols-2 md:grid-cols-3">
    {[
      {
        src: "https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80",
        title: "Mountain View",
        description: "Scenic landscape",
      },
      {
        src: "https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=800&dpr=2&q=80",
        title: "Workspace",
        description: "Modern office setup",
      },
      {
        src: "https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=800&dpr=2&q=80",
        title: "Technology",
        description: "Digital innovation",
      },
    ].map((item, i) => (
      <Card key={i}>
        <AspectRatio ratio={4 / 3}>
          <img
            src={item.src}
            alt={item.title}
            className="size-full object-cover"
          />
        </AspectRatio>
        <CardHeader>
          <CardTitle>{item.title}</CardTitle>
          <CardDescription>{item.description}</CardDescription>
        </CardHeader>
      </Card>
    ))}
  </div>
)