表单控件设计规范
优秀的表单设计能够提高用户完成率和满意度。
输入框设计
基础文本输入
.input {
width: 100%;
padding: 12px 16px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 4px;
font-family: inherit;
transition: border-color 0.2s;
}
.input:hover {
border-color: #bdbdbd;
}
.input:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
.input:disabled {
background-color: #f5f5f5;
color: #999999;
cursor: not-allowed;
}
.input.error {
border-color: #cc0000;
}
.input.success {
border-color: #00cc00;
}
标签和提示
<div class="form-group">
<label for="email" class="form-label">
邮箱地址 <span class="required">*</span>
</label>
<input
id="email"
type="email"
placeholder="user@example.com"
class="input"
aria-describedby="email-hint"
/>
<p id="email-hint" class="form-hint">
我们永远不会分享你的邮箱
</p>
</div>
验证反馈
function FormInput({ label, error, success, helperText, value, onChange, ...props }) {
return (
<div className="form-group">
<label className="form-label">{label}</label>
<input
className={`input ${
error ? 'error' : success ? 'success' : ''
}`}
value={value}
onChange={onChange}
{...props}
/>
{error && (
<p className="form-error" role="alert">
{error}
</p>
)}
{success && (
<p className="form-success">
✓ {success}
</p>
)}
{helperText && (
<p className="form-hint">{helperText}</p>
)}
</div>
);
}
选择框设计
.select {
appearance: none;
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 4px;
background-image: url('data:image/svg+xml;...');
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 40px;
font-size: 16px;
cursor: pointer;
}
.select:hover {
border-color: #bdbdbd;
}
.select:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
复选框和单选按钮
.checkbox-group {
display: flex;
gap: 12px;
align-items: center;
}
.checkbox-input {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #0066cc;
}
.checkbox-label {
cursor: pointer;
user-select: none;
}
/* 自定义复选框 */
.custom-checkbox {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #e0e0e0;
border-radius: 4px;
cursor: pointer;
background-color: white;
transition: all 0.2s;
}
.custom-checkbox:checked {
background-color: #0066cc;
border-color: #0066cc;
background-image: url('data:image/svg+xml;...');
}
.custom-checkbox:focus {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
文本区域
.textarea {
width: 100%;
min-height: 120px;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 4px;
font-family: inherit;
font-size: 16px;
resize: vertical;
transition: border-color 0.2s;
}
.textarea:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
完整表单示例
function SignupForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
subscribe: false,
terms: false,
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [submitted, setSubmitted] = useState(false);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
// 实时验证
if (touched[name]) {
validateField(name, type === 'checkbox' ? checked : value);
}
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
validateField(name, formData[name]);
};
const validateField = (name, value) => {
const newErrors = { ...errors };
switch (name) {
case 'name':
if (!value) newErrors.name = '名字不能为空';
else delete newErrors.name;
break;
case 'email':
if (!value) newErrors.email = '邮箱不能为空';
else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {
newErrors.email = '请输入有效的邮箱';
} else {
delete newErrors.email;
}
break;
case 'password':
if (!value) newErrors.password = '密码不能为空';
else if (value.length < 8) newErrors.password = '密码至少 8 位';
else delete newErrors.password;
break;
case 'confirmPassword':
if (value !== formData.password) {
newErrors.confirmPassword = '两次密码输入不一致';
} else {
delete newErrors.confirmPassword;
}
break;
case 'terms':
if (!value) newErrors.terms = '必须同意服务条款';
else delete newErrors.terms;
break;
default:
break;
}
setErrors(newErrors);
};
const validate = () => {
const newErrors = {};
if (!formData.name) newErrors.name = '名字不能为空';
if (!formData.email) newErrors.email = '邮箱不能为空';
if (formData.password.length < 8) newErrors.password = '密码至少 8 位';
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = '两次密码输入不一致';
}
if (!formData.terms) newErrors.terms = '必须同意服务条款';
return newErrors;
};
const handleSubmit = async (e) => {
e.preventDefault();
// 标记所有字段已触碰
setTouched({
name: true,
email: true,
password: true,
confirmPassword: true,
terms: true,
});
const newErrors = validate();
if (Object.keys(newErrors).length === 0) {
setSubmitted(true);
// 提交表单
console.log('Form submitted:', formData);
// 重置表单
setFormData({
name: '',
email: '',
password: '',
confirmPassword: '',
subscribe: false,
terms: false,
});
} else {
setErrors(newErrors);
}
};
return (
<form onSubmit={handleSubmit} noValidate>
{submitted && (
<div className="form-success-message" role="alert">
注册成功!
</div>
)}
<FormInput
label="姓名"
name="name"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
error={touched.name && errors.name}
helperText="请输入你的全名"
/>
<FormInput
label="邮箱"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
error={touched.email && errors.email}
/>
<FormInput
label="密码"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
error={touched.password && errors.password}
helperText="至少 8 个字符"
/>
<FormInput
label="确认密码"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
error={touched.confirmPassword && errors.confirmPassword}
/>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
name="subscribe"
checked={formData.subscribe}
onChange={handleChange}
className="checkbox-input"
/>
订阅我们的新闻通讯
</label>
</div>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
name="terms"
checked={formData.terms}
onChange={handleChange}
onBlur={handleBlur}
className="checkbox-input"
/>
我同意
<a href="/terms" target="_blank" rel="noopener noreferrer">
服务条款
</a>
和
<a href="/privacy" target="_blank" rel="noopener noreferrer">
隐私政策
</a>
</label>
{touched.terms && errors.terms && (
<p className="form-error">{errors.terms}</p>
)}
</div>
<button type="submit" className="btn btn-primary btn-block">
注册
</button>
</form>
);
}
最佳实践
✅ 应该做的事:
- 使用正确的输入类型
- 提供实时验证反馈
- 清晰的标签和提示
- 最小触摸目标 44x44px
- 支持键盘导航
❌ 不应该做的事:
- 隐藏标签
- 过度使用占位符
- 验证后立即提交
- 忽视无障碍性
- 复杂的验证规则
测试清单
- 所有控件都可用键盘导航
- 标签与输入框关联
- 验证消息清晰
- 色彩对比度足够
- 屏幕阅读器兼容
- 移动设备测试


