仿照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类型)

相关推荐
星栈1 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技1 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569151 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马4 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng4 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding5 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天5 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron
FungLeo8 小时前
成为全栈·Vue 管理后台篇·分类树与标签管理:层级数据如何落到管理界面
前端·javascript·vue.js·分类树·naive ui·标签管理·前端数据结构
Lank_M11 小时前
白底都占八成,PNG 为何只在图表页赢过 JPG
前端
张一凡9311 小时前
给 Coding Agent 加一道"架构闸门":Jin 是如何做到"先审架构、再写代码、不漂移"的
前端·后端·agent