一个输入框加若干附加物(图标、单位、按钮、快捷键提示),共处同一个边框,焦点环 画在整组而不是输入框上。shadcn 的组合,原样收录 —— SearchField 就是拿它搭的。
输入框底下是 Base UI 的 Input,把它放进 Base UI 的 Field.Root 里就自动接线,
不用往下传 props;按钮是 Base UI 的 Button,行与文本域是纯 DOM。
使用
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from '@gedatou/cadenza-ui'<InputGroup>
<InputGroupInput aria-label="搜索" placeholder="搜索..." />
<InputGroupAddon>
<IconSearch aria-hidden />
</InputGroupAddon>
</InputGroup>组成
一行只有两类角色:一个控件,若干 addon —— 按钮、文本、图标都装在 addon 里。
InputGroup
├── InputGroupInput 或 InputGroupTextarea
└── InputGroupAddon # 可以有多个,align 决定摆在哪
├── InputGroupButton
├── InputGroupText
└── 图标 / <kbd> / 任意节点对齐
InputGroupAddon 的 align 决定它摆在哪,默认 inline-start:
配 InputGroupInput 用 inline-*,配 InputGroupTextarea 用 block-*。
block-* 会让 InputGroup 从一行变成竖排、高度随之放开,行内的两个值则不动布局。
摆位置靠的是 CSS order,不是 DOM 顺序 —— 所以装了按钮的 addon 写在控件之后,
Tab 进这一组才先落到输入框。纯图标、纯文本的 addon 不可聚焦,写在前面也一样。
图标
InputGroupAddon 里直接放图标;默认在行首,align="inline-end" 换到行尾:
文本
InputGroupText 用来嵌固定的协议头、单位、域名后缀 —— 用户就不用自己敲了:
按钮
InputGroupButton 底下是 Base UI 的 Button,size 默认 xs,纯图标按钮用 icon-xs:
快捷键提示
addon 里直接放 <kbd>,InputGroup 会替它对齐并收掉多余外边距:
文本域与底部工具条
align="block-end" 的 addon 横跨整行、排到下方,InputGroup 随之从一行变成竖向布局
(block-start 则排到上方)。配 InputGroupTextarea 就是一个带工具条的输入区:
焦点环怎么来的
环画在 InputGroup 上,触发条件是内部某个 data-slot="input-group-control" 的元素
处于 :focus-visible:
has-[[data-slot=input-group-control]:focus-visible]:border-ring
has-[[data-slot=input-group-control]:focus-visible]:ring-3
InputGroupInput / InputGroupTextarea 自带这个 slot,同时用 focus-visible:ring-0
把控件自身的环压掉,所以整组只有一圈。
这个 slot 值是接线契约,不是标记。 别给控件传 data-slot —— 组件是先写属性、
后展开 props,外面传一个同名的会把契约值顶掉,焦点环就此静默消失:不报错,只是不再出现。
要加自己的标记,另起一个属性名。
文本输入框在鼠标点击时也会命中 :focus-visible(浏览器认定它预期要打字),所以点一下
就有环 —— 这是原生行为,不是额外加的。
自定义控件
反过来说:环认的是那个属性,不是具体哪个组件。自己的控件挂上
data-slot="input-group-control" 就接进同一套焦点管理 —— 第三方的自适应高度文本域
这类都能这么塞进来:
import TextareaAutosize from 'react-textarea-autosize'
<InputGroup>
<TextareaAutosize
className="flex-1 resize-none border-0 bg-transparent focus-visible:ring-0"
data-slot="input-group-control"
/>
<InputGroupAddon align="block-end">
<InputGroupButton variant="default">发送</InputGroupButton>
</InputGroupAddon>
</InputGroup>自带的两个控件除了这个属性还压掉了自己的边框和环(border-0 / focus-visible:ring-0),
自定义控件也得照做,否则会看到两圈。
什么时候用 InputGroup
边框归这一行所有,所以 InputGroupInput 自己是没有边框的 —— 单独拿出来用会是个
裸输入框。一旦有东西要和输入框共用边框,再换过来。
状态与 className
InputGroup 没有自己的状态 props:状态挂在里面的控件上,样式画在外面的组上,中间
靠 :has() 连起来。
要整组禁用就禁用里面的控件(或者把整组放进禁用的 Field.Root,Base UI 的 Input
会自己继承)。
has-disabled 这条是坑:组里只要有任何一个 :disabled 元素,整组就按禁用渲染。
SearchField 因此在只读时不渲染清除按钮;hidden 没用,display: none 的元素
仍然会被 :has(:disabled) 命中。
三个 data-slot 可以当选择器用:
Props
顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className。
只有 InputGroupInput 的 className 是双形态(字符串,或 (state) => string)——
它底下是 Base UI 的 Input。其余部件的 className 都是字符串:多数是纯 DOM
元素,而 InputGroupButton 虽然底下是 Base UI 的 Button,类名却要穿过 cva
(它会把函数直接丢掉),所以类型也如实收窄成字符串。
InputGroup
一个 role="group" 的 <div>。边框、圆角、高度和焦点环都在它身上,没有自己的状态
props —— 它靠 :has() 观察后代,四条选择器见状态与 className。
InputGroupAddon
一个 addon 里可以塞多个按钮和图标,它们横向排开:
<InputGroupAddon align="inline-end">
<InputGroupButton>撤销</InputGroupButton>
<InputGroupButton>重做</InputGroupButton>
</InputGroupAddon>点击 addon 的空白处会把焦点送给组内的输入框(点在按钮上时不会)——
这样整个边框范围内哪里都能点出焦点。为了焦点顺序,装了按钮的 addon 要写在控件之后,
用 align 摆位置,见对齐。
InputGroupButton
纯图标按钮必给 aria-label。
<InputGroupButton>订阅</InputGroupButton>
<InputGroupButton aria-label="复制链接" size="icon-xs">
<IconCopy aria-hidden />
</InputGroupButton>InputGroupInput
自带 data-slot="input-group-control"(见上文),去掉了自己的边框和
环,交给外层的组来画。没有可见 label 时记得给 aria-label。
InputGroupTextarea
同样带 input-group-control。组里有它时高度自动放开,不再固定成一行。
InputGroupText
一段静音色的文本,用于单位、协议头、计数。它是 <span>,不接收焦点。
六个部件的 props 类型也一并导出:InputGroupProps / InputGroupAddonProps /
InputGroupButtonProps / InputGroupInputProps / InputGroupTextProps /
InputGroupTextareaProps。