☰
Varlet 选项卡组(Tabs)完全指南:从 Tabs/Tab 到 TabsItems 视图联动的实现原理与实战
2026/10/14 16:57:23 网站建设 项目流程
  • UI组件
  • 前端

【免费下载链接】varlet

A Vue3 component library based on Material Design 2 and 3, supporting mobile and desktop.

项目地址:https://gitcode.com/gh_mirrors/va/varlet
点击查看免费下载

Varlet 的选项卡组由Tabs、Tab、TabsItems、TabItem四个组件协作完成:前者负责选项卡的展示、激活与指示器动画,后者负责内容视图的切换与手势滑动联动。本篇基于 Varlet 仓库文档与源码,完整覆盖选项卡组的全部属性、事件、方法与 CSS 变量,并深入剖析激活匹配(name 优先于 index)、指示器定位、自动滚动阈值、粘性布局等关键实现,帮助你在移动端与桌面端项目中正确落地选项卡交互。

组件家族与文档入口

选项卡组由四个独立注册、通过 Provider 机制协作的组件构成,源码分别位于:

组件标签源码位置职责
VarTabs<var-tabs>Tabs.vue选项卡容器:管理激活态、指示器、滚动、粘性布局
VarTab<var-tab>Tab.vue单个选项卡:点击、颜色状态、水波纹
VarTabsItems<var-tabs-items>TabsItems.vue视图容器:基于swipe实现视图切换与手势滑动
VarTabItem<var-tab-item>TabItem.vue单个视图:承载选项卡对应的内容

四个组件均通过withInstall支持按需安装(见 tabs/index.ts),并借助withPropsDefaultsSetter接入 Varlet 的默认属性定制机制,意味着你可以在全局统一覆盖它们的props默认值。

Tab与TabItem有严格的父子约束:useTabs在 tab/provide.ts 中断言<var-tab/> must in <var-tabs/>,useTabsItems在 tab-item/provide.ts 中断言<var-tab-item/> must in <var-tabs-items/>。脱离父容器使用时会直接抛出断言错误,这是排查“选项卡不渲染”问题时的首要检查点。

基本使用

最小可用形态:Tabs通过v-model:active双向绑定激活标识,子级Tab默认按索引(index)匹配激活。

<script setup> import { ref } from 'vue' const active = ref(0) </script> <template> <var-tabs v-model:active="active"> <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <var-tab>选项3</var-tab> </var-tabs> </template>

active的默认值为0(见 tabs/props.ts),因此不传v-model:active时默认激活第一个选项卡。

具名选项卡

Tabs默认通过active匹配Tab的index来决定激活项,也可以给Tab设置name进行命名匹配,此时Tabs优先匹配name:

<script setup> import { ref } from 'vue' const active = ref('选项1') </script> <template> <var-tabs v-model:active="active"> <var-tab name="选项1">选项1</var-tab> <var-tab name="选项2">选项2</var-tab> <var-tab name="选项3">选项3</var-tab> </var-tabs> </template>

这一优先级在源码中有明确体现。Tabs.vue 的resize方法按顺序执行三级匹配:

const tab: TabProvider | undefined = matchName() || matchIndex() || matchBoundary()

其中matchName遍历子级Tab的name,matchIndex遍历index,matchBoundary则在active为数字且越界时用clamp收敛到合法区间并回写active(Tabs.vue)。Tab侧的激活判断逻辑一致:shouldActive优先比较props.name,否则比较index(Tab.vue)。

修改样式与颜色体系

选项卡组提供了一整套颜色参数,分别控制背景、激活/未激活/禁用文字、指示器:

<template> <var-tabs elevation color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" v-model:active="active" > <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <var-tab>选项3</var-tab> </var-tabs> </template>

各参数在源码中的作用路径:

  • color直接作用于.var-tabs容器的background内联样式(Tabs.vue);
  • active-color、inactive-color、disabled-color由Tabs通过TabsProvider下发给每个Tab,Tab在computeColorStyle中按disabled → active → inactive的优先级输出内联color(Tab.vue)。这与测试用例中的断言一致,如expect(wrapper.find('.var-tab--active').attributes('style')).toContain('color: red;')(tabs/tests/index.spec.js);
  • indicator-color作用于指示器内层--layout-*-indicator-inner,未设置时回退为activeColor(Tabs.vue)。

elevation属性启用海拔阴影。其类型为boolean | string | number,可选true、false或0-24等级;从源码看,Tabs调用formatElevation(elevation, 4),即elevation为true时应用等级 4 的阴影(测试中对应var-elevation--4类名,见 index.spec.js)。

禁用选项

<template> <var-tabs elevation color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" disabled-color="#aaa" v-model:active="active" > <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <var-tab disabled>禁用选项</var-tab> </var-tabs> </template>

禁用态在两个层面生效:Tab的handleClick在disabled为真时直接return,不触发click事件,也不调用onTabClick(Tab.vue);Tabs的resize在命中禁用选项卡时会跳过指示器移动,即禁用选项永远不会被“激活”(Tabs.vue)。

开启滚动

scrollable属性控制选项卡溢出后的横向滚动,可选auto(默认)与always。

<template> <var-tabs elevation color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" v-model:active="active" > <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <!-- ... 更多选项卡 ... --> <var-tab>选项8</var-tab> </var-tabs> </template>

从源码结构看,滚动行为由watchScrollable决定:

function watchScrollable() { localScrollable.value = props.scrollable === 'always' || tabList.length >= 5 }

(Tabs.vue)。也就是说:scrollable="always"无条件开启滚动;scrollable="auto"时在选项卡数量达到 5 个及以上时自动开启。启用滚动后,.var-tabs--layout-*-scrollable类会隐藏滚动条(scrollbar-width: none与::-webkit-scrollbar双重处理,见 tabs.less),且scrollToCenter会在激活切换时将当前选项卡平滑滚动到视口中央,水平/垂直方向分别计算offsetLeft/offsetTop中心偏移(Tabs.vue)。

选项垂直布局与容器垂直布局

选项卡组提供两个相互独立的方向参数:

  • item-direction:单个选项卡内部内容的排布方向(horizontal/vertical),垂直模式下图标与文字上下堆叠,选项卡高度由--tabs-item-vertical-height(66px)控制;
  • layout-direction:整个选项卡组布局方向(horizontal/vertical),垂直模式下选项卡纵向排列,指示器变为左侧(或反转后右侧)的竖条。
<!-- 选项垂直布局 --> <var-tabs elevation item-direction="vertical" color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" v-model:active="active" > <var-tab> <var-icon class="tabs-example-icon" name="information" /> <div>选项1</div> </var-tab> <var-tab> <var-icon class="tabs-example-icon" name="warning" /> <div>选项2</div> </var-tab> <var-tab> <var-icon class="tabs-example-icon" name="error" /> <div>选项3</div> </var-tab> </var-tabs> <style> .tabs-example-icon { margin-bottom: 5px; } </style>
<!-- 容器垂直布局 --> <var-tabs class="tabs-example-vertical" elevation layout-direction="vertical" color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" v-model:active="active" > <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <var-tab>选项3</var-tab> </var-tabs> <style> .tabs-example-vertical { width: 80px !important; height: 120px !important; } </style>

在样式层,方向通过--item-horizontal、--layout-vertical等 BEM 修饰类生效(tabs.less、tab.less)。垂直布局下指示器是左侧 2px 宽竖条,滚动发生在垂直轴(overflow-y: auto),这与水平布局在.var-tabs--layout-vertical中的规则一致。测试用例也验证了这两个属性会动态切换对应的类名(index.spec.js)。

视图联动:TabsItems 与 TabItem

Tabs与TabsItems共享同一个v-model:active,即可实现选项卡与内容视图的双向联动;视图内容基于swipe组件,天然支持手势滑动切换:

<script setup> import { ref } from 'vue' const active = ref(0) </script> <template> <var-tabs elevation color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" v-model:active="active" > <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <var-tab>选项3</var-tab> </var-tabs> <var-tabs-items v-model:active="active"> <var-tab-item>第一个视图的内容</var-tab-item> <var-tab-item>第二个视图的内容</var-tab-item> <var-tab-item>第三个视图的内容</var-tab-item> </var-tabs-items> </template>

联动机制在源码中分为两条通路:

  1. 选项卡 → 视图:TabsItems监听active变化(以及TabItem数量变化,经doubleRaf等待渲染后),执行matchName || matchIndex找到目标视图,先重置所有TabItem的current,再通过swipe.value?.to(index)平滑切换到目标视图(TabsItems.vue);
  2. 视图 → 选项卡:手势滑动完成时,swipe的change事件触发handleSwipeChange,用当前视图的name ?? index回写onUpdate:active,从而反向驱动选项卡切换(TabsItems.vue)。

TabsItems的四个参数都直接映射到底层swipe:can-swipe控制touchable(是否允许滑动切换)、animated控制动画时长(为false时duration置0)、loop控制循环切换、indicator被固定为false隐藏圆点指示器(TabsItems.vue)。getSwipe()方法暴露底层swipe实例,便于调用next()、prev()、to(index)等程序化切换 API,测试用例中即通过wrapper.vm.getSwipe().next()验证了这一链路(index.spec.js)。

TabItem的name属性与Tab的name同理:命名视图时同样遵循“name 优先、index 兜底”的匹配顺序。

开启粘性布局

sticky属性启用粘性(吸顶)布局。从源码看,Tabs的根节点是动态组件:开启sticky时整体被VarSticky包裹,并将sticky-css-mode、offset-top、sticky-z-index透传给 Sticky(Tabs.vue),这三个属性的定义直接复用自stickyProps(见 tabs/props.ts)。

<template> <div class="sticky-example"> <var-tabs sticky elevation color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" v-model:active="active" > <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <var-tab>选项3</var-tab> </var-tabs> </div> </template> <style> .sticky-example { height: 200vh; } </style>

对应地,resizeSticky方法在页面元素位置或尺寸变化后重绘粘性布局,内部调用 Sticky 的resize并以Promise<void>返回(Tabs.vue)。

指示器位置反转

indicator-position控制激活指示器贴边位置,可选normal(默认)与reverse。水平布局下normal指示器贴底、reverse贴顶;垂直布局下normal贴左、reverse贴右(tabs.less)。indicatorPosition计算属性会将其映射为类名后缀-reverse,与布局方向组合生成--layout-*-reverse-indicator修饰类:

<template> <var-tabs elevation indicator-position="reverse" color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" v-model:active="active" > <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <var-tab>选项3</var-tab> </var-tabs> <var-tabs class="tabs-example-vertical" elevation indicator-position="reverse" layout-direction="vertical" color="var(--color-primary)" active-color="var(--color-on-primary)" inactive-color="var(--color-on-info)" v-model:active="active" > <var-tab>选项1</var-tab> <var-tab>选项2</var-tab> <var-tab>选项3</var-tab> </var-tabs> </template> <style> .tabs-example-vertical { width: 80px !important; height: 120px !important; margin-top: 20px; } </style>

激活与指示器动画原理

理解指示器定位对排查“指示器错位/闪烁”很有帮助。指示器是一个绝对定位的div,其尺寸与位置由moveIndicator依据激活选项卡 DOM 实测值计算:

function moveIndicator({ element }: TabProvider) { const el = element.value if (!el) return if (props.layoutDirection === 'horizontal') { indicatorWidth.value = `${el.offsetWidth}px` indicatorX.value = `${el.offsetLeft}px` } else { indicatorHeight.value = `${el.offsetHeight}px` indicatorY.value = `${el.offsetTop}px` } }

(Tabs.vue)。水平方向指示器宽度等于选项卡宽度、通过translateX平移;垂直方向则交换宽高与translateY。样式层为其配置了width与transform上0.3s的过渡(tabs.less),形成滑动动画;indicator-size默认2px,indicator-color未设置时回退active-color。

resize的触发时机覆盖了多种场景:active或scrollable变化(Tabs.vue)、选项卡数量变化(经doubleRaf等待渲染,Tabs.vue)、窗口尺寸变化(onWindowResize)以及keep-alive重新激活(onActivated,Tabs.vue);Tab自身在name或disabled变化时也会触发父级resize(Tab.vue)。因此若你在运行时动态增删选项卡或改变容器宽度,组件会自动重算指示器位置;如遇异步布局变化(如字体加载导致宽度变化),也可显式调用resize方法重绘。

点击链路同样值得注意:Tab点击 →handleClick(disabled时短路)→ 触发Tab的click事件(携带name ?? index与原生Event)→ 调用Tabs的onTabClick,后者依次发出update:active、Tabs的click事件,且仅当激活值真正变化时才发出change事件(Tabs.vue)。这与测试断言expect(onClick).toHaveBeenLastCalledWith(0)、expect(onChange).toHaveBeenLastCalledWith(0)相印证(index.spec.js)。

底部导航场景:fixed-bottom 与 safe-area

fixed-bottom将选项卡组以position: fixed固定在窗口底部(z-index: 99),适合做底部导航;safe-area额外追加env(safe-area-inset-bottom)内边距适配 iPhone 底部安全区(tabs.less)。两者配合使用时,底部导航既能常驻视口底部,又不会遮挡 Home Indicator 区域。

API 参考

属性

Tabs Props
参数说明类型默认值
v-model:active激活的选项卡标识,优先匹配name,其次是indexstring | number0
layout-direction选项卡组的布局方向,可选值为horizontalverticalstringhorizontal
item-direction选项卡的布局方向,可选值为horizontalverticalstringhorizontal
fixed-bottom是否固定在窗口底部,可用做底部导航booleanfalse
color选项卡的背景颜色string-
active-color选项卡激活的主要颜色string-
inactive-color选项卡未激活的主要颜色string-
disabled-color选项卡禁用时的主要颜色string-
indicator-color选项卡激活指示器的颜色,未设置时回退为active-colorstring-
indicator-size选项卡激活指示器的尺寸string | number2px
elevation海拔高度,可选值为truefalse和0-24的等级,true时应用等级 4string | number | booleanfalse
sticky是否启用粘性布局booleanfalse
sticky-css-mode是否启用粘性布局的原生 css sticky 模式booleanfalse
sticky-z-index粘性布局的层级number10
safe-area是否开启底部安全区适配booleanfalse
offset-top吸顶距离string | number0
scrollable是否开启滚动,可选值为autoalwaysstringauto
indicator-position选项卡激活指示器的位置,可选值为normalreversestringnormal
Tab Props
参数说明类型默认值
name选项卡的名字string | number-
disabled是否禁用选项卡booleanfalse
ripple是否启用水波效果booleantrue
TabsItems Props
参数说明类型默认值
v-model:active激活的选项卡的标识string | number0
can-swipe是否允许滑动切换booleantrue
animated是否启用内容切换动画booleantrue
loop是否允许循环切换booleanfalse
TabItem Props
参数说明类型默认值
name视图的名字string | number-

方法

Tabs Methods
方法名说明参数返回值
resize产生位置大小变化时可以调用此方法重绘选项卡组--
resizeSticky产生位置大小变化时可以调用此方法重绘粘性布局-Promise<void>
TabsItems Methods
方法名说明参数返回值
getSwipe获取swipe组件实例--

事件

Tabs Events
事件名说明参数
click点击选项卡时触发,在选项卡disabled状态为true时不触发active: string \| number
change切换选项卡时触发active: string \| number
Tab Events
事件名说明参数
click点击选项卡时触发,在disabled状态为true时不触发active: string \| number
event: Event

插槽

组件插槽名说明参数
Tabsdefault选项卡组的内容-
Tabdefault选项卡内容-
TabsItemsdefault视图组的内容-
TabItemdefault视图的内容-

样式变量

以下为组件使用的 CSS 变量,可以使用StyleProvider组件进行样式定制。

Tabs Variables(tabs.less)
变量名默认值
--tabs-item-horizontal-height44px
--tabs-item-vertical-height66px
--tabs-radius2px
--tabs-padding12px
--tabs-indicator-size2px
--tabs-indicator-border-radius0
--tabs-indicator-backgroundvar(--color-primary)
--tabs-background#fff
--tabs-indicator-inner-size100%
Tab Variables(tab.less)
变量名默认值
--tab-padding12px
--tab-active-colorvar(--color-primary)
--tab-inactive-color#646566
--tab-disabled-colorvar(--color-text-disabled)
--tab-font-sizevar(--font-size-md)
--tab-font-weight400
--tab-active-font-sizevar(--font-size-md)
--tab-active-font-weight400

需要区分的是:内联 props(如active-color)会直接覆盖样式变量对应的类样式,适合一次性局部定制;而 CSS 变量(如--tab-active-color)适合通过StyleProvider做主题级全局定制。

小结

Varlet 选项卡组的设计围绕“Provider 协作 + 单一 active 数据源”展开:Tabs与TabsItems共享v-model:active即可自动完成选项卡、指示器、视图三者的联动,手势滑动与点击切换共用同一条name/index匹配通路。掌握本文的激活匹配顺序(name → index → 越界收敛)、scrollable的 5 个阈值、resize/resizeSticky的调用时机,以及完整的属性与样式变量清单,即可在移动端与桌面端项目中稳定落地选项卡组的各种布局形态。

  • UI组件
  • 前端

【免费下载链接】varlet

A Vue3 component library based on Material Design 2 and 3, supporting mobile and desktop.

项目地址:https://gitcode.com/gh_mirrors/va/varlet
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询