基础控件
在线示例:/design-system/basic(启动 pnpm dev 后打开)。
Button
tsx
import { Button } from '@/components/ui/button'
<Button>主要</Button>
<Button variant="secondary">次要</Button>
<Button variant="outline">描边</Button>
<Button variant="ghost">幽灵</Button>
<Button variant="success">成功</Button>
<Button variant="warning">警告</Button>
<Button variant="destructive">危险</Button>
<Button variant="link">链接</Button>1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
| prop | 类型 | 默认 | 说明 |
|---|---|---|---|
variant | default | secondary | outline | ghost | success | warning | destructive | link | default | 语义变体 |
size | default | xs | sm | lg | icon | icon-xs | icon-sm | icon-lg | default | 尺寸 |
loading | boolean | false | 显示转圈图标并自动禁用,避免重复提交 |
asChild | boolean | false | 用子元素渲染(例如包 <Link>);此时只保留禁用语义,不插图标 |
tsx
<Button variant="outline" loading>提交中</Button>
<Button asChild><Link to="/customers">去列表</Link></Button>1
2
2
为什么 asChild 时不显示加载图标
Radix 的 Slot 只接受单个子元素。加载态需要插入图标,两者同时开启会让 Slot 收到两个 children 并直接崩溃, 所以 asChild 时只保留 disabled / aria-busy。
Input / Textarea
tsx
<Input placeholder="请输入关键字" />
<Input clearable value={keyword} onChange={(e) => setKeyword(e.target.value)} onClear={() => setKeyword('')} />
<Input start={<Search aria-hidden="true" className="size-3.5" />} />
<Input end={<span className="text-xs">.com</span>} />
<Input type="password" defaultValue="runlume-2026" />
<Textarea rows={2} placeholder="补充说明" />1
2
3
4
5
6
2
3
4
5
6
| prop | 类型 | 说明 |
|---|---|---|
start / end | ReactNode | 前后缀插槽(图标、单位、按钮) |
clearable | boolean | 有值时显示清空按钮,配合 onClear |
onClear | () => void | 清空回调(组件不直接改值) |
type | string | password 自动加可见切换;date / time / datetime-local 走日期与时间组件 |
aria-invalid | boolean | 错误态,边框与焦点环转为 danger |
别用"有没有值"决定 DOM 结构
输入框的包裹结构只由属性决定。如果按"是否有值"切换结构,清空按钮出现 / 消失会重挂载 input, 连续输入时会丢焦点。
Checkbox / Switch / RadioGroup
tsx
<label className="flex items-center gap-2 text-sm">
<Checkbox defaultChecked /> 已选中
</label>
<Checkbox indeterminate /> {/* 半选:列表全选、父节点级联 */}
<Switch checked={notify} onCheckedChange={setNotify} aria-label="通知开关" />
<RadioGroup defaultValue="week">
<RadioGroupItem value="week" aria-label="按周" />
</RadioGroup>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
| 组件 | 关键 prop |
|---|---|
Checkbox | checked / defaultChecked、indeterminate、onCheckedChange |
Switch | checked、onCheckedChange、disabled |
RadioGroup | value / defaultValue、onValueChange、orientation |
NativeSelect / Label / Separator / Skeleton
tsx
<Label htmlFor="gallery-select">下拉选择</Label>
<NativeSelect id="gallery-select" defaultValue="a">
<option value="a">选项一</option>
<option value="c" disabled>禁用选项</option>
</NativeSelect>
<Separator />
<Skeleton className="h-4 w-32" />1
2
3
4
5
6
7
2
3
4
5
6
7
原生 select 用于选项少、不需要搜索的场景;需要搜索 / 分组用 Combobox, 需要多选 / 带描述用 Select 与 MultiSelect。
Badge / Tag
tsx
<Badge>实心徽标</Badge>
<Badge variant="outline">描边徽标</Badge>
<Badge variant="secondary" onClose={() => remove(tag)} closeLabel={`移除 ${tag}`}>{tag}</Badge>
<Tag tone="success" icon={<CircleCheck aria-hidden="true" />}>已完成</Tag>
<Tag tone="warning" closable onClose={clear}>待复核</Tag>1
2
3
4
5
6
2
3
4
5
6
| 组件 | 取值 | 用途 |
|---|---|---|
Badge | default | secondary | destructive | outline | ghost | link | 计数、状态附标;传 onClose 变可关闭 |
Tag | default | primary | success | warning | danger | outline | 带语义色的标签,支持图标与关闭 |
Alert
tsx
<Alert variant="info" title="示例环境" description="数据仅用于演示,刷新后恢复初始状态。" />
<Alert
variant="warning"
title="额度即将用尽"
description="本月剩余额度不足 10%,请及时调整用量。"
action={<Button size="sm" variant="outline">查看用量</Button>}
/>1
2
3
4
5
6
7
2
3
4
5
6
7
| prop | 类型 | 说明 |
|---|---|---|
variant | info | success | warning | danger | 四态,图标与配色自动匹配 |
title / description | ReactNode | 标题与正文 |
action | ReactNode | 底部操作区 |
onClose | () => void | 传入后显示关闭按钮 |
Progress
tsx
<Progress value={72} label="导出进度" />
<Progress value={38} tone="warning" label="额度使用" />
<Progress value={12} tone="danger" label="失败比例" />1
2
3
2
3
tone 取 primary | success | warning | danger | info,value / max 控制比例,label 会写到 aria-label。
Kbd
tsx
;<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
打开命令面板1
2
3
4
5
2
3
4
5
NumberField / Slider
tsx
<NumberField label="并发数" value={concurrency} min={1} max={20} onValueChange={setConcurrency} />
<NumberField label="超时" value={timeout} unit="秒" onValueChange={setTimeout} />
<Slider label="告警阈值" value={threshold} onValueChange={setThreshold} max={100} step={5} ticks={[0, 50, 100]} />1
2
3
2
3
| 组件 | 关键 prop |
|---|---|
NumberField | value、onValueChange、min、max、step、unit、label(必填) |
Slider | value: number[]、onValueChange、min / max / step、ticks、label(必填) |
ScrollArea / Collapsible
tsx
<ScrollArea className="h-32 rounded-lg border">
<ul className="divide-y text-sm">…</ul>
</ScrollArea>
<Collapsible>
<CollapsibleTrigger>查看字段说明</CollapsibleTrigger>
<CollapsibleContent>并发数影响同步任务的资源占用,超过 20 需要审批。</CollapsibleContent>
</Collapsible>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
滚动区用于固定高度的长列表;折叠区用于收纳次要说明,不替代页面级的"更多筛选"(那是 SearchFilters 的职责)。