仿照AntDesign,实现一个自定义Tab

一、介绍

Tab 组件是开发中经常使用到的组件,有时直接使用组件自带的组件会比较方便,但是如果有一些自定义需求的时候,这个时候使用Tab组件就会变得比较麻烦,所以,下面我们将仿照Antd Design的Tab组件,实现一个自定义的Tab组件。

  • 目前具备的功能有 主要功能特点:
  1. 支持横向和纵向布局
  2. 支持禁用状态
  3. 支持键盘导航(通过ARIA属性)
  4. 平滑的状态过渡动画
  5. 类型校验(PropTypes)
  6. 响应式设计基础
  7. 支持自定义样式覆盖
  • 与Ant Design的主要差异:
  1. 更简化,没有复杂的状态管理
  2. 更轻量级(无额外依赖)
  3. 基础样式实现,可根据需要扩展
  4. 缺少部分高级功能(如快捷键、动画效果等)

二、具体实现过程

下面是具体的代码合实现过程:

jsx 复制代码
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './CustomTabs.css';

const Tabs = ({ activeKey, onChange, children, tabPosition }) => {
  const [currentActiveKey, setCurrentActiveKey] = useState(activeKey || '');

  const handleTabClick = (key) => {
    if (key !== currentActiveKey) {
      setCurrentActiveKey(key);
      onChange?.(key);
    }
  };

  const getTabs = () => {
    return React.Children.map(children, (child, index) => {
      if (!React.isValidElement(child)) return null;
      
      const { key, disabled, tab } = child.props;
      const isActive = key === currentActiveKey;

      return (
        <div
          key={key || index}
          role="tab"
          className={`tab ${isActive ? 'active' : ''} ${disabled ? 'disabled' : ''}`}
          onClick={() => !disabled && handleTabClick(key || index)}
          aria-selected={isActive}
          aria-disabled={disabled}
        >
          {tab || child.props.children}
        </div>
      );
    });
  };

  const getContent = () => {
    return React.Children.map(children, (child) => {
      if (!React.isValidElement(child)) return null;
      
      const { key } = child.props;
      const isActive = key === currentActiveKey;

      return (
        <div
          role="tabpanel"
          id={`tabpanel-${key}`}
          aria-labelledby={`tab-${key}`}
          className={`tab-content ${isActive ? 'active' : ''}`}
        >
          {child.props.children}
        </div>
      );
    });
  };

  return (
    <div className={`custom-tabs ${tabPosition}`}>
      <div className="tab-list">
        {getTabs()}
      </div>
      <div className="tab-content-wrapper">
        {getContent()}
      </div>
    </div>
  );
};

const TabPane = ({ tab, children }) => {
  return <>{children}</>;
};

Tabs.TabPane = TabPane;

// PropTypes
Tabs.propTypes = {
  activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  onChange: PropTypes.func,
  children: PropTypes.node.isRequired,
  tabPosition: PropTypes.oneOf(['top', 'bottom', 'left', 'right']),
};

export default Tabs;

配套的CSS样式 (CustomTabs.css):

css 复制代码
.custom-tabs {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

.tab-list {
  display: flex;
  border-bottom: 1px solid #f0f0f0;
  position: relative;
}

.tab {
  padding: 12px 24px;
  cursor: pointer;
  border-bottom: 3px solid transparent;
  color: #1890ff;
  transition: all 0.3s;
  font-size: 14px;
  position: relative;
}

.tab:hover {
  color: #40a9ff;
}

.tab.active {
  color: #1890ff;
  border-bottom-color: #1890ff;
  font-weight: 500;
}

.tab.disabled {
  color: #ccc;
  cursor: not-allowed;
}

.tab-content-wrapper {
  padding: 24px;
  border: 1px solid #f0f0f0;
  border-top: none;
}

.tab-content {
  display: none;
}

.tab-content.active {
  display: block;
}

/* 垂直布局 */
.custom-tabs.vertical {
  display: flex;
}

.custom-tabs.vertical .tab-list {
  flex-direction: column;
  border-bottom: none;
  border-right: 1px solid #f0f0f0;
}

.custom-tabs.vertical .tab {
  padding: 16px;
  border-bottom: none;
  border-right: 3px solid transparent;
}

.custom-tabs.vertical .tab.active {
  border-right-color: #1890ff;
}

.custom-tabs.vertical .tab-content-wrapper {
  padding: 24px;
  border: 1px solid #f0f0f0;
  border-left: none;
}

使用示例:

jsx 复制代码
import Tabs from './CustomTabs';

function App() {
  return (
    <div>
      <Tabs defaultActiveKey="1" onChange={(key) => console.log(key)}>
        <Tabs.TabPane tab="Tab 1">
          <p>Content of Tab 1</p>
        </Tabs.TabPane>
        <Tabs.TabPane tab="Tab 2" disabled>
          <p>Content of Tab 2</p>
        </Tabs.TabPane>
        <Tabs.TabPane tab="Tab 3">
          <p>Content of Tab 3</p>
        </Tabs.TabPane>
      </Tabs>

      <Tabs tabPosition="bottom">
        <Tabs.TabPane tab="Tab A">
          <p>Bottom tab content</p>
        </Tabs.TabPane>
      </Tabs>
    </div>
  );
}

三、拓展和其它

可以根据实际需求继续扩展以下功能:

• 动态添加/删除tabs

• 禁用状态样式定制

• 懒加载内容

• 动画过渡效果

• 更多位置选项(如卡片式布局)

• 图标支持

• 类型扩展(如card类型)

相关推荐
excel8 分钟前
JavaScript 对象池:减少高频对象创建与 GC 压力
前端
陈随易2 小时前
纯 Go 写的 JS 引擎,跑分超过了 C 语言的 QuickJS
前端·后端·程序员
子兮曰3 小时前
Chrome 权限元素实战:144 与 151 的渐进增强写法
前端·javascript·chrome
IT_陈寒4 小时前
Redis连接池泄漏那次,我差点把生产环境搞崩了
前端·人工智能·后端
强尼银手4 小时前
我花一天做了个格斗游戏原型,判定全对,然后被「美术」劝退了
前端·javascript
石小石Orz4 小时前
AI 正在蚕食我们的编码能力
前端·后端·算法
陈随易5 小时前
专注飞机 FocusFlight,两个人把番茄钟做成了一趟航班
前端·后端·程序员
总要学点什么5 小时前
第98篇:三维特效-材质线效果——给一条线换十种材质,虚线的图案只填了一半
开发语言·前端·javascript·ecmascript·gis·webgl·材质
10share5 小时前
基于大模型的 i18n 方案:把中文原文当 Key,轻松搞定多语言维护
前端·vue.js
古茗前端团队8 小时前
由 key 引发的思考
前端·javascript·react.js