组件
卡片 Card
将同一主题的内容、状态与操作组织在具备清晰视觉层级与微动效的容器表面中。
基础用法
基础卡片组合包含了标题、描述、右上角轻量操作区、核心内容与底部操作栏:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/card安装依赖库
pnpm add clsx tailwind-merge复制组件源码到
components/ui/card.tsximport * as React from "react"
import { cn } from "@/lib/utils"
const cardSurface = {
/** Lifts the card with a quiet ambient shadow and a low-contrast edge. */
elevated: "border-border/70 border shadow-sm",
/** Flat treatment for dense grids where stacked shadows would be noisy. */
outline: "border-border border shadow-none",
} as const
export interface CardProps extends React.ComponentProps<"div"> {
/** Surface treatment. @default "elevated" */
variant?: keyof typeof cardSurface
/** Adds hover lift, border emphasis and press feedback for cards that behave as a single target. @default false */
interactive?: boolean
}
/** A composable surface that groups related content and actions. */
function Card({
className,
variant = "elevated",
interactive = false,
...props
}: CardProps) {
return (
<div
data-slot="card"
data-variant={variant}
data-interactive={interactive || undefined}
className={cn(
"bg-card text-card-foreground relative isolate flex flex-col gap-5 rounded-xl py-5",
cardSurface[variant],
interactive && [
"focus-visible:ring-ring/40 focus-visible:ring-offset-background cursor-pointer outline-none focus-visible:ring-[3px] focus-visible:ring-offset-2",
"transition-[border-color,box-shadow,translate,scale] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]",
"hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.99] active:duration-100",
"motion-reduce:transition-[border-color,box-shadow] motion-reduce:hover:translate-y-0 motion-reduce:active:scale-100",
variant === "elevated"
? "hover:border-foreground/15 hover:shadow-md"
: "hover:border-foreground/25",
],
className
)}
{...props}
/>
)
}
/** Aligns the card heading, description, and optional action. */
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto] grid gap-x-4 gap-y-1 px-5 sm:px-6",
className
)}
{...props}
/>
)
}
/** The primary heading of a card. */
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"text-balance text-[1.0625rem] font-semibold leading-6 tracking-[-0.015em]",
className
)}
{...props}
/>
)
}
/** Supporting text displayed below the card title. */
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn(
"text-muted-foreground max-w-prose text-pretty text-sm leading-[1.45]",
className
)}
{...props}
/>
)
}
/** Places a compact action opposite the card heading. */
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 -mr-1 -mt-1 self-start",
className
)}
{...props}
/>
)
}
/** Contains the card's main content. */
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-5 sm:px-6", className)}
{...props}
/>
)
}
/**
* Media that bleeds to the card edge. Clips itself to the card radius so the
* card does not need `overflow-hidden`, which would trap nested popovers.
*/
function CardMedia({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-media"
className={cn(
"-mt-5 overflow-hidden rounded-t-[calc(var(--radius)+3px)] [&_img]:block [&_img]:size-full [&_img]:object-cover",
className
)}
{...props}
/>
)
}
/** Aligns secondary information and actions at the end of a card. */
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center gap-2.5 px-5 sm:px-6", className)}
{...props}
/>
)
}
export {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardMedia,
CardTitle,
}
属性 Props
Card
卡片外层容器,继承原生 <div> 元素的全部 HTML 属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| variant | "elevated" | "outline" | "elevated" | 卡片的表面材质与阴影风格。elevated 带有环境阴影;outline 为纯描边扁平风。 |
| interactive | boolean | false | 开启整卡可点击的交互反馈:悬停时上移 2px 并加深边框,按下时轻微回落缩放,键盘聚焦显示焦点环。开启“减少动态效果”时仅保留颜色变化。 |
| className | string | — | 应用于卡片外层容器的额外 CSS 类名。 |
| children | React.ReactNode | — | 卡片内部子结构组件集合。 |
子结构部件
以下各结构部件均继承原生 <div> 元素的全部 HTML 属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| CardHeader | React.ComponentProps<'div'> | — | 卡片头部容器,内部采用 Grid 自动对齐标题、描述与 CardAction 操作区。 |
| CardTitle | React.ComponentProps<'div'> | — | 卡片主标题,默认应用 text-balance 与适宜的粗体字号。 |
| CardDescription | React.ComponentProps<'div'> | — | 标题下方的辅助说明文案,预设浅灰文字色与阅读行宽优化。 |
| CardAction | React.ComponentProps<'div'> | — | 位于卡片右上角的紧凑操作区域,适合放置图标按钮或状态徽章。 |
| CardContent | React.ComponentProps<'div'> | — | 卡片的核心内容区域,具备统一的左右内边距排版。 |
| CardMedia | React.ComponentProps<'div'> | — | 贴顶通栏媒体容器,自带顶部圆角裁切,避免破坏卡片圆角样式。 |
| CardFooter | React.ComponentProps<'div'> | — | 卡片底部操作区,默认 Flex 横向对齐,适合放置主要及次要操作按钮。 |
事件 Events
Card 支持原生 div 元素的所有鼠标与键盘事件,特别适用于交互式卡片:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onClick | (event: React.MouseEvent<HTMLDivElement>) => void | — | 当用户点击整张卡片时触发(需配合 interactive 开启)。 |
| onKeyDown | (event: React.KeyboardEvent<HTMLDivElement>) => void | — | 卡片获得焦点时按下键盘按键触发(如监听 Enter / Space 激活选择)。 |
| onFocus | (event: React.FocusEvent<HTMLDivElement>) => void | — | 卡片通过 Tab 导航获得焦点时触发。 |
| onBlur | (event: React.FocusEvent<HTMLDivElement>) => void | — | 卡片失去焦点时触发。 |
使用场景与设计规范
Card 用于聚合属于同一独立对象或任务的信息与操作(如云主机实例、计费方案、文章摘要或团队空间)。
- 独立可理解:卡片应具备独立的上下文,用户无需结合页面其他孤立区域即可理解卡片所表达的主题。
- 避免多层卡片嵌套:切忌在卡片内部再嵌套多层卡片(Anti-pattern: Card in Card),这会造成过多的边框与阴影杂音。内部区域划分应通过微弱背景底色(
bg-muted/50)、间距或细分隔线解决。 - 点击区域与内部操作:
- 若整张卡片为一个链接/选择项(如选择套餐),使用
interactive并设置清晰的role="radio"或<a>。 - 若卡片内部有多个不同的独立操作(如“重启”、“查看日志”、“删除”),卡片本身不应响应全局点击,操作应明确归集于
CardAction或CardFooter中。
- 若整张卡片为一个链接/选择项(如选择套餐),使用
场景示例
表面风格 (Variants)
提供 elevated(悬浮阴影)与 outline(扁平描边)两种风格:
Loading…
elevated:适合页面中的核心实体卡片,利用轻量阴影建立层次。outline:适合多列密集仪表盘或列表网格,避免多重阴影视觉疲劳。
可交互卡片 (Interactive)
开启 interactive 属性后,卡片具备悬浮抬升、按下微缩与键盘焦点环。下例把三张卡片组织为 radiogroup,支持方向键切换,选中描边通过 motion 的 layoutId 在卡片之间滑动:
Loading…
顶部媒体通栏 (CardMedia)
CardMedia 会自动贴合卡片顶部边缘并进行圆角裁剪,无需在卡片根节点上强制使用 overflow-hidden,从而避免截断卡片内弹出的 Tooltip 或 Popover。下例通过标题链接的 after:absolute after:inset-0 让整卡可点击,悬停时图片轻微放大:
Loading…
紧凑型组合
通过覆盖间距并组合图标、状态标签与按钮,构建精炼的摘要卡片:
Loading…
业务场景:云服务器节点监控卡片
在云控制台或集群面板中,卡片组合了实例规格、CPU/内存实时监控进度条、运行状态及运维操作。点击“重启”可以看到进度条切换为不确定状态:
Loading…
无障碍与交互 Accessibility
- 交互卡片语义:当卡片用作单选卡片时,应添加
role="radio"与aria-checked;当用作跳转卡片时,推荐内部使用<a>或在根节点配置tabIndex={0}与role="button"。 - 键盘支持:对于设置了
interactive的卡片,用户可通过 Tab 键获取焦点,高亮焦点轮廓环(Focus Ring),并通过 Enter 或 Space 触发激活。 - 减弱动态效果:动效规则内置适配
motion-reduce:transition-none,当系统开启减弱动态效果时自动禁用位移抬升。