Skip to Content
@anker-in/headless-ui 2.0 is released 🎉

Input 输入框

文本输入组件,支持多种 HTML5 输入类型、不同尺寸和灵活的插槽系统,用于表单数据收集和用户输入场景。

何时使用

  • 需要用户输入文本、数字、密码、邮箱等信息时
  • 构建表单时需要各种输入字段
  • 需要在输入框中添加图标、按钮等辅助元素时
  • 需要实现搜索框、邮箱订阅等交互场景
  • 需要支持不同尺寸以适应不同的设计需求

快速开始

安装

pnpm add @anker-in/headless-ui

基础用法

import { Input } from '@anker-in/headless-ui' export default function App() { return <Input placeholder="请输入内容" /> }

演示

Input 组件演示

输入框组件的各种使用示例,包括不同类型、尺寸和插槽配置

加载中...
当前视口: 1920px × 600px场景: 默认状态
打开链接

组件结构

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)" />

尺寸规格

尺寸高度文本大小适用场景
sm32px14px紧凑布局、表格内输入
base40px16px默认大小,通用场景
lg48px18px突出显示、大屏幕

在移动端,建议使用 baselg 尺寸,确保至少 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'输入框尺寸
typestring'text'HTML5 输入类型(text, password, email, number, tel, url, search, date, file 等)
placeholderstring-占位符文本
disabledbooleanfalse是否禁用
requiredbooleanfalse是否必填
readOnlybooleanfalse是否只读
valuestring | number-输入值(受控组件)
defaultValuestring | number-默认值(非受控组件)
onChange(e: ChangeEvent<HTMLInputElement>) => void-值变化时的回调
onFocus(e: FocusEvent<HTMLInputElement>) => void-获得焦点时的回调
onBlur(e: FocusEvent<HTMLInputElement>) => void-失去焦点时的回调
classNamestring-自定义 CSS 类名
childrenReactNode-InputSlot 子组件

InputSlot Props

属性类型默认值必需说明
side'left' | 'right''right'插槽位置
classNamestring-自定义 CSS 类名
childrenReactNode-插槽内容(图标、按钮等)

继承的 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-invalidaria-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> 元素
  • 选择正确的输入类型:根据内容使用 emailtelnumber 等类型
  • 提供占位符提示:使用 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 上反馈

Last updated on