Card
A raised surface that holds one thing together: a header, a body and a footer, or a whole link.
cennge-ui
Cennge / public
React UI library on Radix UI and Tailwind CSS v4: dark first, one accent to swap.
- Hover the two cards at the bottom: each is a link, lifts onto a lighter surface and sinks a little while pressed.
- Tab to them: with
asChildthe card is the<a>itself, so it has a focus ring and can be opened in a new tab like any link. - The parts are padded blocks: CardHeader and CardFooter are rows, CardBody a column that takes the free height.
Field marks
- Import
import { Card, CardHeader, CardBody, CardFooter } from "cennge-ui";- Size
- 0.9 KB gzip for this import, its dependencies aside
- Pattern
- A
<div>; your<a>or<button>throughasChild - SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Depth
- surface-3 with a soft shadow: raised on the page and inside a panel alike
- Radius
lg, or the radius set onCenngeProvider- Source
- src/components/card
When to use#
A Card groups what belongs to one thing, a repository, a release, a plan, and sets it apart from what is around it. When the whole card leads somewhere, make the card the link instead of putting a small link inside it.
Similar#
- HoverCard floats over a link while the pointer rests on it; a Card stays in the page.
- Alert is a message about the page, tinted by its colour; a Card holds content.
- Accordion stacks sections that open and close; cards are always open.
Header, body and footer#
CardHeader lines up its children in a row, CardBody stacks them and grows to fill the card, CardFooter is a row of actions. All three are padded divs that take any class.
import { Badge, Button, Card, CardBody, CardFooter, CardHeader } from "cennge-ui";
const changes = [
"Charts, in a new entry: cennge-ui/charts",
"ThemeScript, useTheme and ThemeSwitch: the theme before the first paint",
"The pointer cursor on buttons, and the wait cursor while one loads",
];
export default function ReleaseCard() {
return (
<Card className="w-full max-w-md">
<CardHeader className="items-start">
<div className="min-w-0">
<h3 className="text-lg font-semibold">cennge-ui 0.3.0</h3>
<time dateTime="2026-10-31" className="text-sm text-muted">
Oct 31, 2026
</time>
</div>
<Badge size="sm" color="primary" className="ml-auto">
Minor
</Badge>
</CardHeader>
<CardBody>
<ul className="flex list-disc flex-col gap-1.5 pl-5 text-sm text-muted">
{changes.map((change) => (
<li key={change}>{change}</li>
))}
</ul>
</CardBody>
<CardFooter>
<Button asChild size="sm" variant="flat">
<a href="https://github.com/Cennge/cennge-ui/blob/main/CHANGELOG.md">Changelog</a>
</Button>
<Button asChild size="sm" variant="light">
<a href="https://www.npmjs.com/package/cennge-ui/v/0.3.0">npm</a>
</Button>
</CardFooter>
</Card>
);
}Variants#
raised lifts the card with a shadow. bordered swaps the shadow for an edge, for dense grids and lists. flat has neither and relies on the surface alone.
import { Card, CardBody, type CardVariant } from "cennge-ui";
const cards: { variant: CardVariant; name: string; description: string }[] = [
{ variant: "raised", name: "cennge-ui", description: "React UI library" },
{ variant: "bordered", name: "payload-cache", description: "FastAPI cache service" },
{ variant: "flat", name: "ai-niche-scout", description: "Finds niches worth building for" },
];
export default function CardVariants() {
return (
<div className="grid w-full gap-4 sm:grid-cols-3">
{cards.map((card) => (
<Card key={card.variant} variant={card.variant}>
<CardBody>
<span className="text-xs font-medium text-muted">{card.variant}</span>
<span className="truncate font-semibold">{card.name}</span>
<span className="text-sm text-muted">{card.description}</span>
</CardBody>
</Card>
))}
</div>
);
}A whole card as a link#
asChild renders the card as its child element, here a Next.js Link, and isPressable adds the hover, press and focus styles. It stays a real <a>: the browser shows its address, and it can be opened in a new tab.
import Link from "next/link";
import { Card, CardBody } from "cennge-ui";
const pages = [
{ href: "/docs/installation/", title: "Installation", summary: "npm install and two CSS imports." },
{ href: "/docs/theming/", title: "Theming", summary: "Tokens, surfaces, colour roles and the accent presets." },
{ href: "/docs/dark-mode/", title: "Dark mode", summary: "The stored theme before the first paint." },
];
export default function LinkCards() {
return (
<div className="grid w-full gap-4 sm:grid-cols-3">
{pages.map((page) => (
<Card key={page.href} asChild isPressable>
<Link href={page.href}>
<CardBody>
<span className="font-semibold">{page.title}</span>
<span className="text-sm text-muted">{page.summary}</span>
</CardBody>
</Link>
</Card>
))}
</div>
);
}Props#
Also takes every attribute of <div>.
CardHeader, CardBody and CardFooter take the attributes of a <div>.