Skip to content
cennge-ui
GitHubv0.3.1, changelog

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.

TypeScriptMIT
  1. Hover the two cards at the bottom: each is a link, lifts onto a lighter surface and sinks a little while pressed.
  2. Tab to them: with asChild the card is the <a> itself, so it has a focus ring and can be opened in a new tab like any link.
  3. The parts are padded blocks: CardHeader and CardFooter are rows, CardBody a column that takes the free height.
Live: hover, press, tab.

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> through asChild
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 on CenngeProvider

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.

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.

cennge-ui 0.3.0

Minor
  • 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
release.tsx
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.

raisedcennge-uiReact UI library
borderedpayload-cacheFastAPI cache service
flatai-niche-scoutFinds niches worth building for
variants.tsx
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>
  );
}

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.

link-cards.tsx
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#

PropTypeDefault
variant
"bordered""flat""raised"
"raised"
radius
"none""sm""md""lg""full"
"lg"
isPressable
booleanHover and press feedback. Combine with asChild and an <a> or <button> so it is focusable.
–
fullWidth
boolean
–
asChild
booleanRender the child element (a link, a button) as the card.
–

Also takes every attribute of <div>.

CardHeader, CardBody and CardFooter take the attributes of a <div>.