Card
cardGroups related content into header, body, and footer regions.
Usage
Basic usage
outline stroke card, composed of three sections: CardHeader / CardBody / CardFooter.
<Card variant="outline" className="w-64">
<CardHeader>Hulian Card</CardHeader>
<CardBody> Ancestral temple jade, extremely beautiful and useful. Appearance + ease of use are the primary productivity. </CardBody>
<CardFooter>footer District</CardFooter>
</Card>Floating Card
elevated replaces the solid border with shadow, and the shadow deepens in hover.
<Card variant="elevated" className="w-64">
<CardHeader>Hulian Card</CardHeader>
<CardBody> Ancestral temple jade, extremely beautiful and useful. Appearance + ease of use are the primary productivity. </CardBody>
<CardFooter>footer District</CardFooter>
</Card>Default and compact density
md is the default; sm tightens all three regions together. CardBody no longer assigns a font size, so its content owns typography.
<div className="flex flex-wrap gap-4">
<Card className="w-64">
<CardHeader title="Runtime metrics" />
<CardBody><Text size="lg">98.7%</Text></CardBody>
<CardFooter>Last 5 minutes</CardFooter>
</Card>
<Card size="sm" className="w-64">
<CardHeader title="Runtime metrics" />
<CardBody><Text size="lg">98.7%</Text></CardBody>
<CardFooter>Last 5 minutes</CardFooter>
</Card>
</div>Highlight Card
featured Use primary double-line stroke to highlight recommended items, aligned within the grid without offset.
<Card variant="featured" className="w-64">
<CardHeader>Hulian Card</CardHeader>
<CardBody> Ancestral temple jade, extremely beautiful and useful. Appearance + ease of use are the primary productivity. </CardBody>
<CardFooter>footer District</CardFooter>
</Card>No chrome
plain draws no border, background, or shadow, leaving only the corner radius and the slot roles. Use it when an outer container already provides the chrome, otherwise you get a doubled border.
{/* The outer container owns the chrome, Card owns the structure */}
<div className="rounded-[var(--radius)] border border-primary/40 bg-primary/5 p-1">
<Card variant="plain" className="w-64">
<CardHeader>Hulian Card</CardHeader>
<CardBody> Ancestral temple jade, extremely beautiful and useful. Appearance + ease of use are the primary productivity. </CardBody>
</Card>
</div>None footer
All three sections are optional, only Header + Body is also established.
<Card variant="outline" className="w-64">
<CardHeader>Hulian Card</CardHeader>
<CardBody> Ancestral temple jade, extremely beautiful and useful. Appearance + ease of use are the primary productivity. </CardBody>
</Card>Remove the section rules
divided={false} makes the header and the body read as one block, and tightens the padding the rule used to hold open.
<Card divided={false} className="w-64">
<CardHeader>Pending approvals</CardHeader>
<CardBody>Three items are waiting for review.</CardBody>
</Card>Title / description / extra
Pass title to CardHeader and the heading gets an element of its own: icons and tags on the same row are no longer painted with the header font-medium, and extra aligns itself to the right.
<Card className="w-80">
<CardHeader
title={<>Assign tasks<Tag>By role</Tag></>}
description="Assign in bulk by role, effective immediately"
extra={<Button variant="ghost" size="sm">Expand</Button>}
/>
<CardBody>Three items are waiting for review.</CardBody>
</Card>When to use
Use Card to group related content in a bordered or elevated container. Use List for an item stream or grid, and Descriptions for key-value details. Card is a structural shell with no business behavior.
Import
import { Card, CardHeader, CardBody, CardFooter, Text } from "@hulianui/ui"Props
CardProps inherits HTMLAttributes<HTMLDivElement> and adds a CVA variant:
| Name | Type | Default | Description |
|---|---|---|---|
| variant | "outline" | "elevated" | "featured" | "plain" | "outline" | Border, raised shadow, emphasized, or no chrome at all. |
| size | "sm" | "md" | "md" | Whole-card density. sm tightens the Header, Body, and Footer padding together. |
| divided | boolean | true | Whether a rule separates CardHeader / CardFooter from the body. Setting false removes both rules and tightens the padding they used to hold open. |
CardBody and CardFooter accept native div properties and children. CardBody no longer assigns a font size: its content owns typography, whether you use plain text or an explicit Text size.
CardHeaderProps (also inherits HTMLAttributes<HTMLDivElement> except title, whose type is widened to ReactNode)
| Name | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | - | The heading. It gets an element of its own (data-slot="card-title") and therefore its own size, leading, and weight. |
| description | ReactNode | - | Supporting text below the heading, in the secondary text color. |
| extra | ReactNode | - | Trailing action area (buttons, switches, counts), vertically centered against the heading group and always on the same line: wrapping is decided independently of content length, so a long description never pushes it to a second row. |
"Present" means the same thing it does for PageHeader's meta: null, undefined, false, and "" all count as not passed, so title={isEditing && "Editing"} does not switch layouts when the condition is false.
With none of the three passed, CardHeader stays the bare slot it is today: children is the content and the container carries font-medium. Passing any of them switches to a two-column heading / action layout, and font-medium moves off the container onto the title element, so icons, Tags, and counts on the same row are no longer painted with heading weight. children remains the escape hatch and renders after the title and description, still in the left column.
plain is the no-chrome variant: no border, no background, no shadow, just the corner radius, the text color, and the three slot roles. Use it when the container already gets its appearance from somewhere else: a hero style the page brings along during a migration, an outer card that already draws a frame, or a card sitting inside a gradient section. The other three variants all paint a background (bg-surface), so they produce a doubled border and a doubled surface. The Accordion and Collapsible panels take a plain prop with the same meaning: when the content brings its own appearance, the answer is no skin rather than a different skin.
Example
<Card variant="elevated" className="w-64">
<CardHeader>Hulian Card</CardHeader>
<CardBody>Designed for beauty and practical use.</CardBody>
<CardFooter>Footer</CardFooter>
</Card>md is the default density. Use sm for denser information; all three regions tighten together while the body typography remains owned by its content:
<Card size="sm">
<CardHeader title="Runtime metrics" />
<CardBody>
<Text size="lg">98.7%</Text>
</CardBody>
<CardFooter>Last 5 minutes</CardFooter>
</Card>The "icon + heading + status tag + trailing action" row, the most common admin card header:
<Card>
<CardHeader
title={<><Users className="size-5 text-muted-foreground" />Assign tasks<Tag>By role</Tag></>}
description="Assign in bulk by role; changes take effect immediately."
extra={<Button variant="ghost" size="sm">Expand</Button>}
/>
<CardBody>…</CardBody>
</Card>Usage notes
- The `Card` root carries no padding of its own; content must go inside `CardBody`. The
root only sets the radius, the text color, and the density variables ([--card-body-px:1.25rem]
and friends); the three sections CardHeader / CardBody / CardFooter are what actually
consume those variables. So <Card><div>content</div></Card> has zero padding and sits
flush against the border. That is by design, not a bug. Note also that this library calls it
`CardBody`, not `CardContent` (shadcn/ui uses the latter, so muscle memory misfires here;
when it errors, reach for the right name rather than falling back to a bare div).
- **If padding is gone card-wide, even with
CardBodyin place, suspect a missing@source
in the consumer first** (#336). Those density variables and px-[var(--card-body-px,1.25rem)]
are the only family in the library that writes spacing as an arbitrary value, and such
literals exist nowhere but Hulian's own source. Ordinary classes like px-4, gap-2 and
rounded-xl appear in consumer code too, so Tailwind emits them regardless. The result is
that a missing @source does not look like "no styles at all" but like "**borders, radii and
colors are all correct, yet every container lost its padding**" - which reads as a component
bug. The test: grep card-body-px in the built CSS; no match means @source is missing (see
consuming.md §8). Since @hulianui/tokens 0.12.0 the
preset ships a safelist that covers this family.
- Do not wrap loading skeletons in Card. [[loading-skeletons-are-chromeless-dont-wrap-in-card]] explains why shimmer placeholders conventionally avoid borders and shadows.
- A fixed outer minimum height combined with flex stretching can push a final metadata row outside the card background; see [[grid-card-button-tail-row-leaks-outside-when-outer-min-height]].
- When the heading contains an icon or a
Tag, passtitleinstead of packing the whole row intochildren: insidechildren, the header'sfont-mediumpaints the icon, the tag, and the count with heading weight, while the heading itself gets no size or leading of its own. CardHeader'stitleis aReactNodeand collides with the nativeHTMLAttributes.title?: string, so the type omitstitle. Put a native tooltip on an inner element instead.extranever drops to a second row because `title` or `description` grew (#263). The left column isflex: 1 1 0, so wrapping is decoupled from content length and long text truncates or clamps as written. The flip side is that `extra` keeps its slot even in a narrow card, squeezing the heading -- so give the heading an overflow treatment. Card width comes from the layout (a three-column grid, a sidebar) and has nothing to do with the viewport, which is why there is deliberately no "wrap on narrow screens" step here. PageHeader has one, because a page header is always full width and a narrow viewport does mean a narrow header.divided={false}applies only to the direct childrenCardHeaderandCardFooter, so a nested card does not inherit the outer value and must opt out itself. It is not delivered through context either: Card still has no"use client"and works inside a server component.size="sm"is supplied by density variables on the Card root. A nested Card resets its own defaultmdvalues and must explicitly receivesize="sm"when it should be compact. SectionclassNamevalues such asp-0andpt-0still override the corresponding padding as before.
Related
Table · Book3D · ProTable · PricingTable · JsonViewer · EditableTable
Playground
<Card variant="outline">
<CardHeader>Hulian Card</CardHeader>
<CardBody>...</CardBody>
<CardFooter>footer District</CardFooter>
</Card>