# Card (Caseroom component)

A bordered panel with a title, description and footer. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

74% of the 2,062 sites in The UI Index have a card (1,534 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/card.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/card.json`
- HTML: https://www.themes.dev/caseroom/r/html/card.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/card/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### card.astro

```astro
---
import type { HTMLAttributes } from 'astro/types';
import { cn } from '@/lib/utils';
import { card } from './card.variants';

type Props = Omit<HTMLAttributes<'div'>, 'title'> & { title?: string; description?: string; as?: 'h2' | 'h3' | 'h4' };
const { title, description, as: Heading = 'h3', class: className, ...rest } = Astro.props;
const hasBody = Astro.slots.has('default');
const hasFooter = Astro.slots.has('footer');
---

<div class={cn(card.root, className)} {...rest}>
  {
    (title || description) && (
      <div class={card.header}>
        {title && <Heading class={card.title}>{title}</Heading>}
        {description && <p class={card.description}>{description}</p>}
      </div>
    )
  }
  {hasBody && <div class={card.content}><slot /></div>}
  {hasFooter && <div class={card.footer}><slot name="footer" /></div>}
</div>
```

### card.tsx

```tsx
import type { HTMLAttributes, ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { card } from './card.variants';

export type CardProps = Omit<HTMLAttributes<HTMLDivElement>, 'title'> & {
  title?: ReactNode;
  description?: ReactNode;
  footer?: ReactNode;
  /** The heading level of the title (default h3), so cards fit the page outline. */
  as?: 'h2' | 'h3' | 'h4';
};

/** A bordered panel with an optional title, description and footer. The same props in Astro and React. */
export function Card({ title, description, footer, as: Heading = 'h3', className, children, ...props }: CardProps) {
  return (
    <div className={cn(card.root, className)} {...props}>
      {(title || description) && (
        <div className={card.header}>
          {title && <Heading className={card.title}>{title}</Heading>}
          {description && <p className={card.description}>{description}</p>}
        </div>
      )}
      {children && <div className={card.content}>{children}</div>}
      {footer && <div className={card.footer}>{footer}</div>}
    </div>
  );
}
```

### card.variants.ts

```tsx
export const card = {
  root: 'flex flex-col gap-5 rounded-xl border bg-card py-5 text-card-foreground',
  header: 'grid gap-1.5 px-5',
  title: 'text-base leading-tight font-semibold tracking-[-0.01em]',
  description: 'text-sm text-muted-foreground',
  content: 'px-5 text-sm',
  footer: 'flex items-center gap-2 border-t px-5 pt-4',
};
```
