Input 输入框
文本输入组件,支持多种 HTML5 输入类型、不同尺寸和灵活的插槽系统,用于表单数据收集和用户输入场景。
何时使用
- 需要用户输入文本、数字、密码、邮箱等信息时
- 构建表单时需要各种输入字段
- 需要在输入框中添加图标、按钮等辅助元素时
- 需要实现搜索框、邮箱订阅等交互场景
- 需要支持不同尺寸以适应不同的设计需求
快速开始
安装
pnpm
pnpm add @anker-in/headless-ui基础用法
import { Input } from '@anker-in/headless-ui'
export default function App() {
return <Input placeholder="请输入内容" />
}演示
组件结构
Input 是一个灵活的复合组件,由以下部分构成:
import { Input, InputSlot } from '@anker-in/headless-ui'
// 基础输入框
<Input placeholder="请输入" />
// 带插槽的输入框
<Input placeholder="搜索">
<InputSlot side="left">
<SearchIcon />
</InputSlot>
</Input>组件说明
| 组件 | 用途 | 典型内容 |
|---|---|---|
Input | 输入框主体 | 文本输入区域,可包含 InputSlot 子组件 |
InputSlot | 插槽区域 | 图标、按钮或其他辅助元素 |
尺寸(Sizes)
Input 提供三种尺寸选项:
<Input size="sm" placeholder="小尺寸 (32px)" />
<Input size="base" placeholder="中尺寸 (40px) - 默认" />
<Input size="lg" placeholder="大尺寸 (48px)" />尺寸规格
| 尺寸 | 高度 | 文本大小 | 适用场景 |
|---|---|---|---|
sm | 32px | 14px | 紧凑布局、表格内输入 |
base | 40px | 16px | 默认大小,通用场景 |
lg | 48px | 18px | 突出显示、大屏幕 |
在移动端,建议使用 base 或 lg 尺寸,确保至少 40px 的高度以提供良好的触摸体验。
输入类型(Input Types)
Input 支持所有 HTML5 标准输入类型:
文本输入
<Input type="text" placeholder="文本输入" />密码输入
<Input type="password" placeholder="密码输入" />邮箱输入
<Input type="email" placeholder="邮箱地址" />数字输入
<Input type="number" placeholder="数字输入" min={0} max={100} />电话输入
<Input type="tel" placeholder="电话号码" />URL 输入
<Input type="url" placeholder="网址" />搜索输入
<Input type="search" placeholder="搜索内容" />日期选择
<Input type="date" />文件上传
<Input type="file" accept="image/*" />插槽功能(Slots)
使用 InputSlot 在输入框左侧或右侧添加图标、按钮等元素。
左侧图标
import { Input, InputSlot } from '@anker-in/headless-ui'
import { SearchIcon } from 'lucide-react'
<Input placeholder="搜索内容">
<InputSlot side="left">
<SearchIcon className="h-4 w-4 text-gray-400" />
</InputSlot>
</Input>右侧按钮
import { Input, InputSlot, Button } from '@anker-in/headless-ui'
<Input className="rounded-3xl pr-0" placeholder="输入邮箱">
<InputSlot side="right">
<Button size="sm" className="h-full rounded-none">
订阅
</Button>
</InputSlot>
</Input>双侧插槽
import { Input, InputSlot } from '@anker-in/headless-ui'
import { DollarSign, HelpCircle } from 'lucide-react'
<Input placeholder="金额">
<InputSlot side="left">
<DollarSign className="h-4 w-4" />
</InputSlot>
<InputSlot side="right">
<HelpCircle className="h-4 w-4 cursor-help" />
</InputSlot>
</Input>状态(States)
禁用状态
<Input disabled placeholder="禁用状态" />禁用状态的样式特征:
cursor-not-allowed:鼠标指针显示禁止符号opacity-50:透明度降低至 50%- 无法获得焦点或输入
必填字段
<Input required placeholder="必填字段" />只读状态
<Input readOnly value="只读内容" />错误状态
通过自定义样式显示错误状态:
<Input
placeholder="邮箱地址"
className="border-red-500 focus-visible:ring-red-500"
aria-invalid="true"
aria-describedby="email-error"
/>
<span id="email-error" className="text-sm text-red-500">
请输入有效的邮箱地址
</span>Props API
Input Props
| 属性 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
| size | 'sm' | 'base' | 'lg' | 'base' | ❌ | 输入框尺寸 |
| type | string | 'text' | ❌ | HTML5 输入类型(text, password, email, number, tel, url, search, date, file 等) |
| placeholder | string | - | ❌ | 占位符文本 |
| disabled | boolean | false | ❌ | 是否禁用 |
| required | boolean | false | ❌ | 是否必填 |
| readOnly | boolean | false | ❌ | 是否只读 |
| value | string | number | - | ❌ | 输入值(受控组件) |
| defaultValue | string | number | - | ❌ | 默认值(非受控组件) |
| onChange | (e: ChangeEvent<HTMLInputElement>) => void | - | ❌ | 值变化时的回调 |
| onFocus | (e: FocusEvent<HTMLInputElement>) => void | - | ❌ | 获得焦点时的回调 |
| onBlur | (e: FocusEvent<HTMLInputElement>) => void | - | ❌ | 失去焦点时的回调 |
| className | string | - | ❌ | 自定义 CSS 类名 |
| children | ReactNode | - | ❌ | InputSlot 子组件 |
InputSlot Props
| 属性 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
| side | 'left' | 'right' | 'right' | ❌ | 插槽位置 |
| className | string | - | ❌ | 自定义 CSS 类名 |
| children | ReactNode | - | ✅ | 插槽内容(图标、按钮等) |
继承的 Props
Input 组件继承所有标准 HTML <input> 元素的属性,包括:
name:表单字段名称id:元素唯一标识autoComplete:自动完成提示autoFocus:自动获得焦点maxLength:最大字符长度minLength:最小字符长度pattern:正则表达式验证min,max,step:数字输入限制accept:文件上传类型限制multiple:文件上传多选aria-*:无障碍属性
类型定义
import { ComponentPropsWithout, RemovedProps } from '@anker-in/headless-ui'
import { VariantProps } from 'class-variance-authority'
// Input Props
export interface InputProps
extends ComponentPropsWithout<'input', 'size'>,
VariantProps<typeof inputVariants> {
// 所有标准 input 属性
// size: 'sm' | 'base' | 'lg'
// children: ReactNode (InputSlot components)
}
// InputSlot Props
interface InputSlotProps extends ComponentPropsWithout<'div', RemovedProps> {
/**
* 插槽的位置
* @default 'right'
*/
side?: 'left' | 'right'
}
// 使用示例
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, size, children, ...props }, ref) => {
// 组件实现
}
)
const InputSlot = React.forwardRef<HTMLDivElement, InputSlotProps>(
({ className, side = 'right', children, ...props }, ref) => {
// 组件实现
}
)
Input.displayName = 'Input'
InputSlot.displayName = 'InputSlot'
export { Input, InputSlot }使用示例
基础表单输入
import { Input } from '@anker-in/headless-ui'
export default function BasicInput() {
return (
<div className="space-y-4">
<div>
<label htmlFor="username" className="mb-2 block text-sm font-medium">
用户名
</label>
<Input
id="username"
type="text"
placeholder="请输入用户名"
required
/>
</div>
<div>
<label htmlFor="email" className="mb-2 block text-sm font-medium">
邮箱地址
</label>
<Input
id="email"
type="email"
placeholder="your@email.com"
required
/>
</div>
</div>
)
}受控组件
import { useState } from 'react'
import { Input } from '@anker-in/headless-ui'
export default function ControlledInput() {
const [value, setValue] = useState('')
return (
<div>
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="输入内容"
/>
<p className="mt-2 text-sm text-gray-600">
当前输入: {value}
</p>
</div>
)
}搜索框
import { Input, InputSlot } from '@anker-in/headless-ui'
import { Search } from 'lucide-react'
export default function SearchInput() {
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const query = formData.get('search')
console.log('搜索:', query)
}
return (
<form onSubmit={handleSearch}>
<Input
name="search"
type="search"
placeholder="搜索产品..."
className="w-full"
>
<InputSlot side="left">
<Search className="h-5 w-5 text-gray-400" />
</InputSlot>
</Input>
</form>
)
}邮箱订阅
import { useState } from 'react'
import { Input, InputSlot, Button } from '@anker-in/headless-ui'
export default function EmailSubscribe() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const handleSubscribe = async () => {
setLoading(true)
try {
// 模拟 API 调用
await new Promise((resolve) => setTimeout(resolve, 1000))
alert(`订阅成功: ${email}`)
setEmail('')
} finally {
setLoading(false)
}
}
return (
<div className="max-w-md">
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="输入您的邮箱"
className="rounded-3xl pr-0"
required
>
<InputSlot side="right">
<Button
size="sm"
className="h-full rounded-none"
loading={loading}
onClick={handleSubscribe}
disabled={!email}
>
订阅
</Button>
</InputSlot>
</Input>
</div>
)
}密码输入(带显示/隐藏)
import { useState } from 'react'
import { Input, InputSlot } from '@anker-in/headless-ui'
import { Eye, EyeOff } from 'lucide-react'
export default function PasswordInput() {
const [showPassword, setShowPassword] = useState(false)
return (
<div>
<label htmlFor="password" className="mb-2 block text-sm font-medium">
密码
</label>
<Input
id="password"
type={showPassword ? 'text' : 'password'}
placeholder="请输入密码"
>
<InputSlot side="right">
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="cursor-pointer text-gray-500 hover:text-gray-700"
aria-label={showPassword ? '隐藏密码' : '显示密码'}
>
{showPassword ? (
<EyeOff className="h-5 w-5" />
) : (
<Eye className="h-5 w-5" />
)}
</button>
</InputSlot>
</Input>
</div>
)
}表单验证
import { useState } from 'react'
import { Input } from '@anker-in/headless-ui'
export default function ValidationInput() {
const [email, setEmail] = useState('')
const [error, setError] = useState('')
const validateEmail = (value: string) => {
if (!value) {
setError('邮箱地址不能为空')
return false
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setError('请输入有效的邮箱地址')
return false
}
setError('')
return true
}
const handleBlur = () => {
validateEmail(email)
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value)
if (error) validateEmail(e.target.value)
}
return (
<div>
<label htmlFor="email-validation" className="mb-2 block text-sm font-medium">
邮箱地址
</label>
<Input
id="email-validation"
type="email"
value={email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="your@email.com"
className={error ? 'border-red-500 focus-visible:ring-red-500' : ''}
aria-invalid={!!error}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && (
<p id="email-error" className="mt-1 text-sm text-red-500">
{error}
</p>
)}
</div>
)
}数字输入(带步进按钮)
import { useState } from 'react'
import { Input, InputSlot } from '@anker-in/headless-ui'
import { Plus, Minus } from 'lucide-react'
export default function NumberInput() {
const [value, setValue] = useState(0)
const increment = () => setValue((v) => v + 1)
const decrement = () => setValue((v) => Math.max(0, v - 1))
return (
<div className="max-w-xs">
<label className="mb-2 block text-sm font-medium">数量</label>
<Input
type="number"
value={value}
onChange={(e) => setValue(Number(e.target.value))}
min={0}
className="text-center"
>
<InputSlot side="left">
<button
type="button"
onClick={decrement}
className="px-3 text-gray-600 hover:text-gray-900"
aria-label="减少"
>
<Minus className="h-4 w-4" />
</button>
</InputSlot>
<InputSlot side="right">
<button
type="button"
onClick={increment}
className="px-3 text-gray-600 hover:text-gray-900"
aria-label="增加"
>
<Plus className="h-4 w-4" />
</button>
</InputSlot>
</Input>
</div>
)
}可访问性
键盘支持
| 按键 | 说明 |
|---|---|
Tab | 移动焦点到输入框或下一个可聚焦元素 |
Shift + Tab | 移动焦点到上一个可聚焦元素 |
Enter | 提交表单(如果输入框在 form 内) |
Esc | 清除搜索输入(type=“search” 时) |
Arrow Up/Down | 增减数字值(type=“number” 时) |
ARIA 属性
Input 组件支持所有标准 ARIA 属性,常用的有:
aria-label:为没有关联<label>的输入框提供标签aria-labelledby:关联外部标签元素aria-describedby:关联帮助文本或错误消息aria-invalid="true":标记输入内容无效aria-required="true":标记必填字段(也可以使用required属性)
最佳实践
始终提供清晰的标签
为每个输入框提供 <label> 元素或 aria-label 属性,确保屏幕阅读器用户理解输入框的用途。
// ✅ 推荐:使用 label 元素
<label htmlFor="email">邮箱地址</label>
<Input id="email" type="email" />
// ✅ 推荐:使用 aria-label
<Input aria-label="搜索产品" type="search" />使用正确的输入类型
根据输入内容选择合适的 type 属性,这样可以:
- 在移动设备上显示正确的键盘
- 启用浏览器内置的验证
- 提供更好的用户体验
<Input type="email" /> {/* 邮箱键盘 */}
<Input type="tel" /> {/* 电话键盘 */}
<Input type="number" /> {/* 数字键盘 */}
<Input type="url" /> {/* URL 键盘 */}提供清晰的错误提示
使用 aria-invalid 和 aria-describedby 关联错误消息,确保错误信息对屏幕阅读器可访问。
<Input
aria-invalid={!!error}
aria-describedby={error ? 'error-message' : undefined}
/>
{error && <span id="error-message">{error}</span>}确保足够的对比度
输入框边框、文本和占位符文本的对比度应符合 WCAG AA 标准(至少 4.5:1)。
提供自动完成提示
使用 autoComplete 属性帮助浏览器自动填充,提高表单填写效率。
<Input type="email" autoComplete="email" />
<Input type="tel" autoComplete="tel" />
<Input type="text" autoComplete="name" />测试屏幕阅读器
使用 VoiceOver(macOS)、NVDA(Windows)或其他屏幕阅读器测试输入框的可访问性,确保:
- 标签正确朗读
- 错误消息可访问
- 插槽内容有意义
- 焦点顺序合理
最佳实践
✅ 推荐做法
- 使用语义化标签:为每个输入框提供清晰的
<label>元素 - 选择正确的输入类型:根据内容使用
email、tel、number等类型 - 提供占位符提示:使用
placeholder显示输入格式示例 - 实时验证反馈:在用户输入或失去焦点时提供即时验证反馈
- 支持自动完成:使用
autoComplete属性提高表单填写效率
// ✅ 正确示例
<div>
<label htmlFor="phone" className="mb-2 block text-sm font-medium">
手机号码
</label>
<Input
id="phone"
type="tel"
placeholder="13812345678"
autoComplete="tel"
pattern="[0-9]{11}"
required
aria-describedby="phone-hint"
/>
<p id="phone-hint" className="mt-1 text-xs text-gray-500">
请输入11位手机号码
</p>
</div>❌ 避免做法
- 不要省略标签:仅使用
placeholder不足以提供可访问性 - 不要使用模糊的占位符:避免 “请输入” 这类不具体的提示
- 不要在输入时移除占位符:这会让用户忘记要输入什么
- 不要禁用自动完成:除非有明确的安全原因(如一次性密码)
- 不要忽略移动端体验:确保输入框在移动设备上足够大且易于点击
// ❌ 错误示例
<Input placeholder="请输入" /> {/* 缺少标签,占位符不明确 */}
// ❌ 错误示例
<Input
type="text"
autoComplete="off" {/* 不必要地禁用自动完成 */}
/>
// ✅ 正确示例
<label htmlFor="username">用户名</label>
<Input
id="username"
type="text"
placeholder="字母或数字,4-20位"
autoComplete="username"
/>常见问题
如何自定义输入框的圆角?
通过 className 覆盖默认的 rounded-md 样式:
<Input className="rounded-full" placeholder="全圆角输入框" />
<Input className="rounded-none" placeholder="无圆角输入框" />如何限制输入长度?
使用 maxLength 属性限制字符数:
<Input
type="text"
maxLength={20}
placeholder="最多20个字符"
/>如何实现输入框自动获取焦点?
使用 autoFocus 属性:
<Input autoFocus placeholder="页面加载时自动聚焦" />谨慎使用 autoFocus,过度使用会影响用户体验,特别是在模态框或对话框中。
如何在 InputSlot 中添加点击事件?
InputSlot 内的元素可以正常响应点击事件:
<Input placeholder="搜索">
<InputSlot side="right">
<button
type="button"
onClick={() => console.log('点击了搜索图标')}
className="px-2"
>
<Search className="h-5 w-5" />
</button>
</InputSlot>
</Input>如何处理文件上传?
使用 type="file" 并通过 onChange 获取文件:
import { useState } from 'react'
import { Input } from '@anker-in/headless-ui'
export default function FileUpload() {
const [file, setFile] = useState<File | null>(null)
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0]
if (selectedFile) {
setFile(selectedFile)
}
}
return (
<div>
<Input
type="file"
onChange={handleFileChange}
accept="image/*"
/>
{file && <p className="mt-2">已选择: {file.name}</p>}
</div>
)
}如何实现防抖(debounce)?
使用自定义 Hook 实现防抖:
import { useState, useEffect } from 'react'
import { Input } from '@anker-in/headless-ui'
function useDebounce(value: string, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
export default function DebouncedInput() {
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearchTerm = useDebounce(searchTerm, 500)
useEffect(() => {
if (debouncedSearchTerm) {
console.log('搜索:', debouncedSearchTerm)
// 执行搜索逻辑
}
}, [debouncedSearchTerm])
return (
<Input
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="输入搜索内容(500ms 防抖)"
/>
)
}如何与 React Hook Form 集成?
Input 组件完全兼容 React Hook Form:
import { useForm } from 'react-hook-form'
import { Input, Button } from '@anker-in/headless-ui'
interface FormData {
username: string
email: string
}
export default function HookFormExample() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>()
const onSubmit = (data: FormData) => {
console.log('提交数据:', data)
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label htmlFor="username" className="mb-2 block text-sm font-medium">
用户名
</label>
<Input
id="username"
{...register('username', {
required: '用户名不能为空',
minLength: { value: 3, message: '用户名至少3个字符' },
})}
placeholder="请输入用户名"
aria-invalid={!!errors.username}
/>
{errors.username && (
<p className="mt-1 text-sm text-red-500">{errors.username.message}</p>
)}
</div>
<div>
<label htmlFor="email" className="mb-2 block text-sm font-medium">
邮箱地址
</label>
<Input
id="email"
type="email"
{...register('email', {
required: '邮箱不能为空',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '请输入有效的邮箱地址',
},
})}
placeholder="your@email.com"
aria-invalid={!!errors.email}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-500">{errors.email.message}</p>
)}
</div>
<Button type="submit">提交</Button>
</form>
)
}相关资源
更新日志
v2.0.0 (2025-01-17)
- 🎉 初始版本发布
- ✨ 支持三种尺寸(sm, base, lg)
- ✨ 支持所有 HTML5 输入类型
- ✨ 插槽系统支持左右侧添加图标、按钮
- ♿ 完整的无障碍支持(ARIA 属性、键盘导航)
- 🎨 基于 CVA 的类型安全变体系统
- 📱 响应式设计,移动端友好
- 📝 完善的 TypeScript 类型定义
本文档有帮助吗?
在 GitHub 上反馈