组件
描述列表 Descriptions
以清晰对齐的键值结构展示对象、记录与档案详情,支持跨列合并、边框网格与上下/左右排版。
基础用法
最基础的描述列表用法。使用 Descriptions 容器配合 DescriptionsItem 声明标签与对应内容;默认无边框样式下标签与标题左对齐,适合嵌入详情页正文:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/descriptions安装基础依赖与工具函数
pnpm add clsx tailwind-merge复制组件源码到
components/ui/descriptions.tsximport * as React from "react"
import { cn } from "@/lib/utils"
export interface DescriptionsItemProps extends Omit<
React.ComponentProps<"td">,
"colSpan"
> {
/** Term associated with the item value. */
label: React.ReactNode
/** Number of description columns occupied by this item. @default 1 */
span?: 1 | 2 | 3 | 4
/** Classes applied to the label cell. */
labelClassName?: string
/** Classes applied to the value cell. */
contentClassName?: string
}
/** Declares one label-value pair inside Descriptions. */
function DescriptionsItem(_props: DescriptionsItemProps) {
return null
}
DescriptionsItem.displayName = "DescriptionsItem"
export interface DescriptionsProps extends Omit<
React.ComponentProps<"div">,
"title"
> {
/** Heading rendered above the description list. */
title?: React.ReactNode
/** Action rendered opposite the heading. */
extra?: React.ReactNode
/** Number of item groups per row. @default 3 */
columns?: 1 | 2 | 3 | 4
/** Draw a structured border around labels and values. @default false */
bordered?: boolean
/** Place labels beside or above their values. @default "horizontal" */
layout?: "horizontal" | "vertical"
/** Vertical density. @default "default" */
size?: "sm" | "default"
/** Fixed width of every horizontal label cell. @default "7rem" */
labelWidth?: React.CSSProperties["width"]
}
type DescriptionItemElement = React.ReactElement<DescriptionsItemProps>
type DescriptionCell = { item: DescriptionItemElement; span: number }
/** Packs items into rows; the last item of an incomplete row stretches to fill it. */
function groupItems(children: React.ReactNode, columns: number) {
const items = React.Children.toArray(children).filter(
React.isValidElement
) as DescriptionItemElement[]
const rows: DescriptionCell[][] = []
let row: DescriptionCell[] = []
let occupied = 0
const flush = () => {
const last = row.at(-1)
if (last) last.span += columns - occupied
rows.push(row)
row = []
occupied = 0
}
items.forEach((item) => {
const span = Math.min(item.props.span ?? 1, columns)
if (occupied > 0 && occupied + span > columns) flush()
row.push({ item, span })
occupied += span
if (occupied === columns) flush()
})
if (row.length) flush()
return rows
}
/** A table-aligned semantic key-value list for records, profiles, and object summaries. */
function Descriptions({
className,
children,
title,
extra,
columns = 3,
bordered = false,
layout = "horizontal",
size = "default",
labelWidth = "7rem",
...props
}: DescriptionsProps) {
const rows = groupItems(children, columns)
const horizontal = layout === "horizontal"
const cellPadding = size === "sm" ? "px-3 py-2" : "px-4 py-3"
const compactPadding = size === "sm" ? "py-1" : "py-2"
const normalizedLabelWidth =
typeof labelWidth === "number" ? `${labelWidth}px` : labelWidth
return (
<div
data-slot="descriptions"
data-bordered={bordered || undefined}
data-layout={layout}
data-size={size}
className={cn("min-w-0", className)}
{...props}
>
{title || extra ? (
<div className="mb-3 flex min-h-8 items-center justify-between gap-4">
{title ? (
<div
data-slot="descriptions-title"
className="font-semibold tracking-tight"
>
{title}
</div>
) : (
<span />
)}
{extra ? (
<div data-slot="descriptions-extra" className="shrink-0">
{extra}
</div>
) : null}
</div>
) : null}
<div data-slot="descriptions-table-wrap" className="overflow-x-auto">
<table
data-slot="descriptions-table"
className={cn(
"w-full table-fixed border-collapse text-left text-sm",
bordered && "border"
)}
style={{ minWidth: columns > 1 ? `${columns * 13}rem` : undefined }}
>
<colgroup>
{Array.from({ length: columns }, (_, index) => (
<React.Fragment key={index}>
<col
style={
horizontal ? { width: normalizedLabelWidth } : undefined
}
/>
{horizontal ? <col /> : null}
</React.Fragment>
))}
</colgroup>
<tbody>
{rows.flatMap((row, rowIndex) => {
if (horizontal) {
return (
<tr key={rowIndex}>
{row.map(({ item, span }, itemIndex) => {
const {
label,
span: _span,
labelClassName,
contentClassName,
className: itemClassName,
children: value,
...itemProps
} = item.props
return (
<React.Fragment key={item.key ?? itemIndex}>
<th
scope="row"
data-slot="descriptions-label"
className={cn(
"text-muted-foreground align-top font-medium",
bordered
? cn(cellPadding, "bg-muted/45 border")
: cn(compactPadding, "pr-4"),
labelClassName
)}
>
{label}
</th>
<td
data-slot="descriptions-value"
colSpan={span * 2 - 1}
className={cn(
"text-foreground min-w-0 break-words align-top",
bordered
? cn(cellPadding, "border")
: cn(compactPadding, "pr-6"),
itemClassName,
contentClassName
)}
{...itemProps}
>
{value}
</td>
</React.Fragment>
)
})}
</tr>
)
}
return [
<tr key={`${rowIndex}-labels`}>
{row.map(({ item, span }, itemIndex) => {
return (
<th
key={item.key ?? itemIndex}
scope="col"
colSpan={span}
data-slot="descriptions-label"
className={cn(
"text-muted-foreground align-top font-medium",
bordered
? cn(cellPadding, "bg-muted/45 border")
: "pr-6 pb-1",
item.props.labelClassName
)}
>
{item.props.label}
</th>
)
})}
</tr>,
<tr key={`${rowIndex}-values`}>
{row.map(({ item, span }, itemIndex) => {
const {
span: _span,
label: _label,
labelClassName: _labelClassName,
contentClassName,
className: itemClassName,
children: value,
...itemProps
} = item.props
return (
<td
key={item.key ?? itemIndex}
colSpan={span}
data-slot="descriptions-value"
className={cn(
"text-foreground min-w-0 break-words align-top",
bordered
? cn(cellPadding, "border")
: cn("pr-6", size === "sm" ? "pb-3" : "pb-4"),
itemClassName,
contentClassName
)}
{...itemProps}
>
{value}
</td>
)
})}
</tr>,
]
})}
</tbody>
</table>
</div>
</div>
)
}
export { Descriptions, DescriptionsItem }
属性 Props
Descriptions
Descriptions 是外层容器组件,用于统一设定全局排版方向、列数、边框风格与密度:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| title | React.ReactNode | — | 列表顶部左侧的主标题内容。 |
| extra | React.ReactNode | — | 列表顶部右侧的操作区内容(如编辑、刷新或更多操作按钮)。 |
| columns | 1 | 2 | 3 | 4 | 3 | 一行内展示的键值对列数。 |
| bordered | boolean | false | 是否为表格与每个单元格绘制网格线边框,并为标签单元格填充浅色背景。 |
| layout | "horizontal" | "vertical" | "horizontal" | 标签与数值的相对排列方向。horizontal 为左右并排,vertical 为上下堆叠。 |
| size | "sm" | "default" | "default" | 单元格内边距的视觉密度。 |
| labelWidth | React.CSSProperties['width'] | "7rem" | 水平布局(horizontal)下所有标签单元格的固定宽度,支持像素或 rem 单位。 |
| className | string | — | 应用于最外层容器的额外 CSS 类名。 |
DescriptionsItem
用于声明单条“标签—值”数据项:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| label | React.ReactNode | — | 数据项的标题/标签文本,具有明确的语义提示作用。 |
| span | 1 | 2 | 3 | 4 | 1 | 该数据项在当前行中所占据的列宽跨度(超过外层 columns 时按 columns 计算)。一行未排满时,该行最后一项会自动延展占满剩余列,边框网格保持完整。 |
| labelClassName | string | — | 应用于当前项标签单元格(th)的额外 CSS 类名。 |
| contentClassName | string | — | 应用于当前项内容单元格(td)的额外 CSS 类名。 |
| children | React.ReactNode | — | 当前项的具体内容或值。 |
事件 Events
Descriptions 本身作为只读结构化展示容器不直接发射业务事件,所有交互事件由嵌套在其中的子控件(如 extra 中的操作按钮、数据单元格内的复制按钮或链接)自行处理:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onClick | (event: React.MouseEvent<HTMLDivElement>) => void | — | 在容器上点击时触发的原生 DOM 事件。 |
使用场景与设计规范
Descriptions 专门用于呈现单个特定对象、记录或档案的属性全貌(如用户详情卡片、服务器实例参数、订单履约概览、合约条款)。
- 何时使用 Descriptions vs Table:
- Descriptions:专注于单个对象的多维度属性(每个字段含义不同,如“名称”、“创建时间”、“状态”、“配置详情”)。
- Table:专注于多个同构对象的列表横向比对(每行结构相同,如“用户列表”、“日志记录”)。
- 标签文案简明性:标签文案应简练明确(如“创建时间”而非“该条记录创建于系统的时间”);对于补充性业务说明,请放在对应的值内容中或通过 Tooltip 辅助说明。
- 跨列合理性(Span):对于长文本、多标签列表或进度条等较宽内容,设置
span={2}或span={3}占满整行,避免内容被挤压产生过长折行。 - 边框风格选择:
- 无边框(默认):视觉更轻量透气,适合嵌入卡片内部或个人主页等轻量场景。
- 带边框(
bordered):网格线清晰,标签与内容色彩分明,适合企业级管理后台、配置审批与工单详情页。
场景示例
带边框多列详情(实例档案)
在服务器或设备档案详情中,使用 bordered 模式搭配 span 跨列合并,结合 Badge 与复制按钮丰富信息呈现。最后一行只有一项时会自动延展占满整行:
Loading…
上下排版布局(订阅概览)
当空间横向较为受限,或者希望像仪表盘看板一样优先阅读指标数值时,使用 layout="vertical" 与 size="sm":
Loading…
无障碍与交互 Accessibility
- 表格语义与读屏支持:底层基于原生
<table>构建,水平布局时标签自动赋予<th scope="row">,垂直布局时标签赋予<th scope="col">,屏幕阅读器可以清晰地向视障用户读出属性名与属性值之间的关联。 - 自适应横向滚动:当在极窄视口(如手机屏幕)下浏览多列描述列表时,组件外层自动包裹
overflow-x-auto容器并设定最小列宽计算,保障内容不重叠、文字不异常断裂。 - 高对比度设计:标签单元格采用
text-muted-foreground,内容单元格采用text-foreground,在深色与浅色模式下均满足 WCAG AA 级对比度要求。