按钮组件设计详解

HTMLPAGE 团队
18 分钟阅读

学习按钮样式、交互状态、无障碍性和最佳实践

按钮组件设计详解

按钮是 UI 中最重要的交互元素。优秀的按钮设计能够指导用户行为。

按钮类型

Primary Button(主按钮)

.btn-primary {
  background-color: #0066cc;
  color: #ffffff;
  padding: 12px 24px;
  border: none;
  border-radius: 4px;
  font-weight: 600;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.btn-primary:hover {
  background-color: #0052a3;
  box-shadow: 0 4px 12px rgba(0, 102, 204, 0.2);
}

.btn-primary:active {
  background-color: #003d7a;
  transform: scale(0.98);
}

.btn-primary:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
  opacity: 0.6;
}

Secondary Button(次按钮)

.btn-secondary {
  background-color: transparent;
  color: #0066cc;
  border: 2px solid #0066cc;
  padding: 10px 22px;
  border-radius: 4px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-secondary:hover {
  background-color: rgba(0, 102, 204, 0.1);
}

.btn-secondary:active {
  background-color: rgba(0, 102, 204, 0.2);
}

Danger Button(危险按钮)

.btn-danger {
  background-color: #cc0000;
  color: #ffffff;
  padding: 12px 24px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
}

.btn-danger:hover {
  background-color: #990000;
  box-shadow: 0 4px 12px rgba(204, 0, 0, 0.2);
}

交互状态

Loading 状态

import { useState } from 'react';

function Button({ children, onClick, loading, ...props }) {
  const [isLoading, setIsLoading] = useState(false);
  
  const handleClick = async () => {
    setIsLoading(true);
    try {
      await onClick();
    } finally {
      setIsLoading(false);
    }
  };
  
  return (
    <button
      onClick={handleClick}
      disabled={isLoading || loading}
      aria-busy={isLoading || loading}
      {...props}
    >
      {isLoading ? (
        <>
          <span className="spinner" aria-hidden="true"></span>
          {children}
        </>
      ) : (
        children
      )}
    </button>
  );
}

Disabled 状态

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background-color: #cccccc;
  color: #999999;
}

/* 禁用状态下隐藏指针光标 */
.btn:disabled:hover {
  box-shadow: none;
  transform: none;
}

Focus 状态

.btn:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1),
              0 0 0 5px #0066cc;
}

/* 键盘导航焦点 */
.btn:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

按钮大小

/* 小按钮 */
.btn-sm {
  padding: 6px 12px;
  font-size: 12px;
  min-height: 32px;
  min-width: 32px;
}

/* 中等按钮(默认) */
.btn-md {
  padding: 12px 24px;
  font-size: 16px;
  min-height: 44px;
  min-width: 44px;
}

/* 大按钮 */
.btn-lg {
  padding: 16px 32px;
  font-size: 18px;
  min-height: 56px;
  min-width: 56px;
}

/* 全宽按钮 */
.btn-block {
  width: 100%;
  display: block;
}

无障碍性

<!-- 语义正确 -->
<button type="submit" aria-label="提交表单">
  提交
</button>

<!-- 加载状态 -->
<button aria-busy="true" disabled>
  <span aria-hidden="true" class="spinner"></span>
  加载中...
</button>

<!-- 图标按钮 -->
<button aria-label="关闭">
  <svg aria-hidden="true" width="24" height="24">
    <line x1="18" y1="6" x2="6" y2="18" />
    <line x1="6" y1="6" x2="18" y2="18" />
  </svg>
</button>

<!-- 切换按钮 -->
<button aria-pressed="false" aria-label="点赞">
  ♥
</button>

完整组件示例

const Button = React.forwardRef((
  {
    children,
    variant = 'primary',
    size = 'md',
    loading = false,
    disabled = false,
    icon,
    className,
    ...props
  },
  ref
) => {
  return (
    <button
      ref={ref}
      className={`btn btn-${variant} btn-${size} ${className}`}
      disabled={disabled || loading}
      aria-busy={loading}
      {...props}
    >
      {icon && <span className="btn-icon" aria-hidden="true">{icon}</span>}
      {loading ? (
        <>
          <span className="spinner" aria-hidden="true"></span>
          {children}
        </>
      ) : (
        children
      )}
    </button>
  );
});

Button.displayName = 'Button';

最佳实践

应该做的事:

  • 最小触摸目标 44x44px
  • 清晰的视觉反馈
  • 使用语义 HTML <button>
  • 提供加载状态反馈
  • 支持键盘导航

不应该做的事:

  • 使用 <div> 模拟按钮
  • 隐藏焦点指示器
  • 过多的按钮样式
  • 忽视禁用状态
  • 使用 <a> 代替按钮

测试清单

  • 在各种浏览器中测试
  • 验证键盘导航
  • 检查色彩对比度
  • 测试触摸设备
  • 屏幕阅读器兼容性