Components · Layout
Card
A bordered panel with a title, description and footer.
74% of the 2,062 sites in The UI Index have a card (1,534 sites).
Invite your team
Teammates can edit every project in this workspace.
Seats are billed per month. You can remove a member at any time.
Invite your team
Teammates can edit every project in this workspace.
Seats are billed per month. You can remove a member at any time.
Both previews render the same markup; the HTML tab below is that markup without a framework.
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
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.
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>
);
}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',
};---
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>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',
};<div class="flex flex-col gap-5 rounded-xl border bg-card py-5 text-card-foreground w-full max-w-sm">
<div class="grid gap-1.5 px-5">
<h3 class="text-base leading-tight font-semibold tracking-[-0.01em]">Invite your team</h3>
<p class="text-sm text-muted-foreground">Teammates can edit every project in this workspace.</p>
</div>
<div class="px-5 text-sm"> Seats are billed per month. You can remove a member at any time. </div>
<div class="flex items-center gap-2 border-t px-5 pt-4">
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] h-8 px-3 text-[13px]">
Send invite
</button>
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground active:bg-pressed h-8 px-3 text-[13px]">
Later
</button>
</div>
</div>