表单控件设计规范

AI Content Team
20 分钟阅读

学习输入框、选择框、复选框等表单控件的设计和实现

表单控件设计规范

优秀的表单设计能够提高用户完成率和满意度。

输入框设计

基础文本输入

.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
  • 支持键盘导航

不应该做的事:

  • 隐藏标签
  • 过度使用占位符
  • 验证后立即提交
  • 忽视无障碍性
  • 复杂的验证规则

测试清单

  • 所有控件都可用键盘导航
  • 标签与输入框关联
  • 验证消息清晰
  • 色彩对比度足够
  • 屏幕阅读器兼容
  • 移动设备测试