Card
card把相关内容装进带页头、正文和页脚的卡片
用法
基础用法
outline 描边卡片,由 CardHeader / CardBody / CardFooter 三段组成。
<Card variant="outline" className="w-64">
<CardHeader>瑚琏卡片</CardHeader>
<CardBody>宗庙玉器,至美又大用。颜值 + 好用是第一生产力。</CardBody>
<CardFooter>footer 区</CardFooter>
</Card>浮起卡片
elevated 用阴影替代实线边框,hover 时阴影加深。
<Card variant="elevated" className="w-64">
<CardHeader>瑚琏卡片</CardHeader>
<CardBody>宗庙玉器,至美又大用。颜值 + 好用是第一生产力。</CardBody>
<CardFooter>footer 区</CardFooter>
</Card>默认与紧凑密度
md 默认;sm 同时收紧三个分区的间距。CardBody 不再指定字号,文本由内容自己决定。
<div className="flex flex-wrap gap-4">
<Card className="w-64">
<CardHeader title="运行指标" />
<CardBody><Text size="lg">98.7%</Text></CardBody>
<CardFooter>最近 5 分钟</CardFooter>
</Card>
<Card size="sm" className="w-64">
<CardHeader title="运行指标" />
<CardBody><Text size="lg">98.7%</Text></CardBody>
<CardFooter>最近 5 分钟</CardFooter>
</Card>
</div>高亮卡片
featured 用 primary 双线描边突出推荐项,网格内对齐不偏移。
<Card variant="featured" className="w-64">
<CardHeader>瑚琏卡片</CardHeader>
<CardBody>宗庙玉器,至美又大用。颜值 + 好用是第一生产力。</CardBody>
<CardFooter>footer 区</CardFooter>
</Card>不画皮
plain 不画边框/底色/阴影,只留圆角与插槽语义。外皮由外层容器提供时用它,否则会双重描边。
{/* 外皮归外层容器,结构归 Card */}
<div className="rounded-[var(--radius)] border border-primary/40 bg-primary/5 p-1">
<Card variant="plain" className="w-64">
<CardHeader>瑚琏卡片</CardHeader>
<CardBody>宗庙玉器,至美又大用。颜值 + 好用是第一生产力。</CardBody>
</Card>
</div>无 footer
三段皆可选,仅 Header + Body 也成立。
<Card variant="outline" className="w-64">
<CardHeader>瑚琏卡片</CardHeader>
<CardBody>宗庙玉器,至美又大用。颜值 + 好用是第一生产力。</CardBody>
</Card>去掉分区分隔线
divided={false} 让标题区与正文成为同一块,并顺带收掉分隔线撑着的那段内边距。
<Card divided={false} className="w-64">
<CardHeader>待办审批</CardHeader>
<CardBody>三条待办等待处理。</CardBody>
</Card>标题 / 副标题 / 右侧操作
CardHeader 传 title 后标题有自己的元素:同一行的图标与标签不再被 header 的 font-medium 染成标题字重,extra 自己对齐右侧。
<Card className="w-80">
<CardHeader
title={<>指派任务<Tag>按角色</Tag></>}
description="按角色批量指派,指派后立即生效"
extra={<Button variant="ghost" size="sm">展开</Button>}
/>
<CardBody>三条待办等待处理。</CardBody>
</Card>何时用
把一组相关内容圈进带边框/阴影的容器——信息块、统计项、表单分区、列表卡片。需要条目流用 List(其 grid 态本身就是卡片栅格);展示键值对详情用 Descriptions。本组件只是纯容器外壳 + 三段插槽,不含业务逻辑。
导入
import { Card, CardHeader, CardBody, CardFooter, Text } from "@hulianui/ui"Props
CardProps 继承原生 HTMLAttributes<HTMLDivElement>,外加 CVA 变体:
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| variant | "outline" | "elevated" | "featured" | "plain" | "outline" | 外观:描边 / 投影抬升 / 强调 / 不画皮 |
| size | "sm" | "md" | "md" | 整卡密度:sm 同时收紧 Header / Body / Footer 的内边距 |
| divided | boolean | true | 是否用分隔线把 CardHeader / CardFooter 与正文切开。设 false 时两条线一起去掉,并把它们原本撑着的那段内边距收一档 |
CardBody / CardFooter 为插槽容器,接收原生 div 属性 + children。CardBody 不再指定字号;正文排版由内容自己拥有,直接放文本或显式使用 Text size 都可以。
CardHeaderProps(另继承 HTMLAttributes<HTMLDivElement>,除 title,因其类型被改为 ReactNode)
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| title | ReactNode | - | 主标题。标题有自己的元素(data-slot="card-title"),因此有独立的字号 / 行高 / 字重 |
| description | ReactNode | - | 副标题 / 说明,排在标题下方,次要文字色 |
| extra | ReactNode | - | 右侧操作区(按钮、开关、计数),与标题群恒同行垂直居中:换行判据不看内容长度,description 再长也不会把它挤到第二行 |
「有值」的口径与 PageHeader 的 meta 一致:null / undefined / false / "" 都算没传,所以 title={isEditing && "编辑中"} 在假值时不会切进结构态。
三者一个都不传时 CardHeader 就是今天的裸插槽:children 直接作正文,容器带 font-medium。传了任意一个即切换为「标题群 / 右侧操作区」两列排布,此时 font-medium 从容器上撤掉、只落在标题元素上——同一行的图标、Tag、计数不再被染成标题字重。children 保留为逃生口,结构态下排在标题与副标题之后(仍在左列)。
plain 是「不画皮」的那一档:不画边框、不铺底色、不投阴影,只留圆角、文字色和三段插槽语义。用在容器的外观已经由别处提供的场景——迁移期页面自带的 hero 样式、外层已经有一层卡片、或者卡片坐在带渐变的区块里。其余三档都会画底色(bg-surface),套上去就是双重描边 + 双重底色。同名的 plain 在 Accordion / Collapsible 的 Panel 上也有,语义一致:内容自带外观时,要的不是改皮肤而是没有皮肤。
禁忌 / 坑
- `Card` 根节点自己不带任何内边距,内容必须放进 `CardBody`。 根上只有圆角、文字色与
密度变量([--card-body-px:1.25rem] 这类),真正吃这些变量的是 CardHeader / CardBody
/ CardFooter 三个分区。所以 <Card><div>内容</div></Card> 是零内边距,紧贴边框 ——
这是设计如此,不是 bug。顺带一提本库叫 `CardBody`,不叫 `CardContent`(shadcn/ui 是
后者,写惯了容易顺手打错,报错后别退回裸 div)。
- 整卡内边距都塌了、连 `CardBody` 也没救回来时,先怀疑消费方漏配 `@source`(#336)。
上面那些密度变量与 px-[var(--card-body-px,1.25rem)] 是全库唯一一族 arbitrary value 间距,
只有瑚琏源码里才有这种字面量;而 px-4 / gap-2 / rounded-xl 这些常规类消费方自己代码
里也写、照样生成。于是漏配 @source 的症状不是「组件没样式」,而是「**边框圆角颜色全对,
唯独容器内边距塌掉**」,看着像组件 bug。判据:构建产物 CSS 里 grep card-body-px,搜不到
就是 @source 漏了(见 consuming.md §8)。
@hulianui/tokens 0.12.0 起 preset 带 safelist 兜住了这一族。
- 别用 Card 包 loading 骨架屏——参见 [[loading-skeletons-are-chromeless-dont-wrap-in-card]]:骨架按惯例是无边框无阴影的纯 shimmer 块,套 Card 会显得过重。
- 列表/侧栏里 Card 末行(时间戳/meta 行)若设了外层
min-height又用 flex 撑高,meta 行可能漏到卡片背景外——参见 [[grid-card-button-tail-row-leaks-outside-when-outer-min-height]]。 - 标题里放图标 /
Tag时用title而不是把整行塞进children:塞进children时 header 的font-medium会连图标、标签、计数一起染成标题字重,而标题自己反而没有字号与行高的表达。 CardHeader的title是ReactNode,与原生HTMLAttributes.title?: string冲突,类型已Omit<"title">——需要原生 tooltip 请挂在内层元素上。extra不会因为 `title` / `description` 变长而掉到第二行(#263):左列是flex: 1 1 0,换行判据与内容长度脱钩,长文本该truncate/line-clamp就截断。这也意味着窄容器里 `extra` 会一直占着位置——想让它挤压标题就得给标题写溢出处理。卡片宽度由布局给(三列网格、侧栏),与视口无关,所以这里刻意没有「窄屏换行」那一档;页面级的 PageHeader 才有,那边页头总是全宽、视口窄等于页头窄。divided={false}只作用于 Card 的直接子CardHeader/CardFooter,卡里套卡时外层的取值不会传染给内层(内层要关线自己传)。它也不是 context 下发——Card 至今没有"use client",能直接放进 server component 里用。size="sm"由 Card 根节点提供密度变量;嵌套的 Card 会重设自己的默认md值,想紧凑要显式传size="sm"。分区的className(例如p-0/pt-0)仍可按原有契约覆盖对应内边距。
相关
Table · Book3D · ProTable · PricingTable · JsonViewer · EditableTable
Playground
<Card variant="outline">
<CardHeader>瑚琏卡片</CardHeader>
<CardBody>...</CardBody>
<CardFooter>footer 区</CardFooter>
</Card>