wui
组件

卡片 Card

将同一主题的内容、状态与操作组织在具备清晰视觉层级与微动效的容器表面中。

基础用法

基础卡片组合包含了标题、描述、右上角轻量操作区、核心内容与底部操作栏:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/card
安装依赖库
pnpm add clsx tailwind-merge
复制组件源码到 components/ui/card.tsx
components/ui/card.tsx
import * 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 为纯描边扁平风。
interactivebooleanfalse开启整卡可点击的交互反馈:悬停时上移 2px 并加深边框,按下时轻微回落缩放,键盘聚焦显示焦点环。开启“减少动态效果”时仅保留颜色变化。
classNamestring—应用于卡片外层容器的额外 CSS 类名。
childrenReact.ReactNode—卡片内部子结构组件集合。

子结构部件

以下各结构部件均继承原生 <div> 元素的全部 HTML 属性:

属性类型默认值说明
CardHeaderReact.ComponentProps<'div'>—卡片头部容器,内部采用 Grid 自动对齐标题、描述与 CardAction 操作区。
CardTitleReact.ComponentProps<'div'>—卡片主标题,默认应用 text-balance 与适宜的粗体字号。
CardDescriptionReact.ComponentProps<'div'>—标题下方的辅助说明文案,预设浅灰文字色与阅读行宽优化。
CardActionReact.ComponentProps<'div'>—位于卡片右上角的紧凑操作区域,适合放置图标按钮或状态徽章。
CardContentReact.ComponentProps<'div'>—卡片的核心内容区域,具备统一的左右内边距排版。
CardMediaReact.ComponentProps<'div'>—贴顶通栏媒体容器,自带顶部圆角裁切,避免破坏卡片圆角样式。
CardFooterReact.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,当系统开启减弱动态效果时自动禁用位移抬升。