Card
Composable surface for grouping related content. Headers, descriptions, content slots, and footers - opt in to the parts you need.
Overview
Card is the cornerstone display primitive: a rounded surface with a structured Header to Content to Footer rhythm. Each sub-component is a thin styled div with a data-slot attribute, so the API stays composable rather than configurative. You opt in to the parts you need: a marketing card might use only CardHeader and CardContent; a dashboard tile might add CardAction for a top-right control and CardFooter for a trend caption.
Card is intentionally surface-only. It doesn't manage focus, hover, or selection - wrap it in a Button (via asChild) or a Link when the whole card should be interactive.
Preview
Compose primitives directly. Skip the lock-in.
'use client';import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from '@gremorie/rx-display';import { Button } from '@gremorie/rx-forms';export function CardPreview() { return ( <Card className="max-w-sm"> <CardHeader> <CardTitle>Welcome to Gremorie</CardTitle> <CardDescription> AI-native design system, registry first. </CardDescription> </CardHeader> <CardContent> <p className="text-sm text-muted-foreground"> Compose primitives directly. Skip the lock-in. </p> </CardContent> <CardFooter> <Button>Get started</Button> </CardFooter> </Card> );}Anatomy
Card the surface (bg-card, rounded, bordered, vertical flow)
├─ CardHeader title/description area; lays out a CardAction to the right when present
│ ├─ CardTitle semibold heading line
│ ├─ CardDescription muted supporting copy
│ └─ CardAction top-right slot (button, menu, switch)
├─ CardContent the main body (padded horizontally)
└─ CardFooter bottom row (actions, captions, metadata)Installation
bash npx gremorie@latest add rx-card bash pnpm dlx gremorie@latest add rx-card bash yarn dlx gremorie@latest add rx-card bash bunx --bun gremorie@latest add rx-card Usage
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@gremorie/rx-display";
import { Button } from "@gremorie/rx-forms";
export function Example() {
return (
<Card className="max-w-sm">
<CardHeader>
<CardTitle>Welcome to Gremorie</CardTitle>
<CardDescription>AI-native design system, registry first.</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Compose primitives directly. Skip the lock-in.
</p>
</CardContent>
<CardFooter>
<Button>Get started</Button>
</CardFooter>
</Card>
);
}npx gremorie@latest add ng-cardimport { Component } from '@angular/core';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from '@gremorie/ng-display';
import { Button } from '@gremorie/ng-forms';
@Component({
selector: 'app-example',
standalone: true,
imports: [
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
Button,
],
template: `
<gr-card class="max-w-sm">
<gr-card-header>
<gr-card-title>Welcome to Gremorie</gr-card-title>
<gr-card-description
>AI-native design system, registry first.</gr-card-description
>
</gr-card-header>
<gr-card-content>
<p class="text-sm text-muted-foreground">
Compose primitives directly. Skip the lock-in.
</p>
</gr-card-content>
<gr-card-footer>
<ai-button>Get started</ai-button>
</gr-card-footer>
</gr-card>
`,
})
export class ExampleComponent {}API
<Card>
The root surface. Renders a rounded div with border, background, and a flex flex-col gap-6 rhythm.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Extra classes merged via cn. Use for layout (width, max-width, margin), not for surface colors. |
All other div props are forwarded.
<CardHeader>
Container for the title, description, and optional action. Uses a grid so CardAction can sit top-right while title and description stack on the left.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Extra classes merged via cn. |
<CardTitle>
Section heading. Renders as a div with leading-none font-semibold.
<CardDescription>
Secondary copy under the title. Renders as a div with text-muted-foreground text-sm.
<CardAction>
Top-right slot inside CardHeader for a control (icon button, menu trigger, badge). Uses CSS grid positioning, so the order in JSX doesn't matter - CardHeader detects it via has-data-[slot=card-action] and switches to a two-column layout.
<CardContent>
Main body. Horizontal padding only - vertical rhythm comes from the root gap-6.
<CardFooter>
Footer row. Defaults to flex items-center; add border-t on CardFooter (or border-b on CardHeader) and the slot expands its vertical padding via [.border-t]:pt-6.
Composition
<Card>is the surface. Always the outermost element.<CardHeader>holdsCardTitle,CardDescription, and optionallyCardAction.<CardContent>is the body. Drop in paragraphs, charts, lists, forms.<CardFooter>holds actions or summary text. Optional.
Use the full composition rather than dumping everything into CardContent - the named slots give downstream consumers clear hooks to override or theme.
Variations
Card with action
$48,210
'use client';import { Badge, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from '@gremorie/rx-display';export function CardWithActionPreview() { return ( <Card className="max-w-sm"> <CardHeader> <CardTitle>Monthly revenue</CardTitle> <CardDescription>Across all active workspaces.</CardDescription> <CardAction> <Badge variant="secondary">+12.4%</Badge> </CardAction> </CardHeader> <CardContent> <p className="text-2xl font-semibold">$48,210</p> </CardContent> </Card> );}CardAction sits in the top-right of CardHeader via CSS grid - JSX order does not matter. CardHeader detects it through has-data-[slot=card-action] and switches to a two-column layout.
Card with bordered footer
Up to 10 seats are included on the Pro plan.
'use client';import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from '@gremorie/rx-display';import { Button } from '@gremorie/rx-forms';export function CardWithFooterPreview() { return ( <Card className="max-w-sm"> <CardHeader> <CardTitle>Invite teammates</CardTitle> <CardDescription> They will get an email with a join link. </CardDescription> </CardHeader> <CardContent> <p className="text-sm text-muted-foreground"> Up to 10 seats are included on the Pro plan. </p> </CardContent> <CardFooter className="border-t"> <Button variant="outline" className="w-full"> Send invites </Button> </CardFooter> </Card> );}The border-t on CardFooter triggers [.border-t]:pt-6, expanding the footer's top padding so the separator reads cleanly.
Accessibility
- Structural, not semantic by default: Card renders as a plain
div. It is not announced as a region. - Add
role="region"andaria-labelledbywhen the card represents a distinct section that should be navigable as a landmark (e.g. a dashboard widget). Pointaria-labelledbyat theCardTitle'sid. - Make the whole card clickable by wrapping
CardHeader's title in an<a>and stretching with::after, or by wrapping the entireCardin aButtonviaasChildpatterns. Don't addonClickto the bareCard- that's a div without keyboard support. - Focus order follows DOM order;
CardActionis positioned via grid, so it visually sits top-right but receives focus in the order it appears in JSX (after the title and description, by default).