- UI组件
- 前端
【免费下载链接】varlet
A Vue3 component library based on Material Design 2 and 3, supporting mobile and desktop.
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>联动机制在源码中分为两条通路:
- 选项卡 → 视图:
TabsItems监听active变化(以及TabItem数量变化,经doubleRaf等待渲染后),执行matchName || matchIndex找到目标视图,先重置所有TabItem的current,再通过swipe.value?.to(index)平滑切换到目标视图(TabsItems.vue); - 视图 → 选项卡:手势滑动完成时,
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,其次是index | string | number | 0 |
layout-direction | 选项卡组的布局方向,可选值为horizontalvertical | string | horizontal |
item-direction | 选项卡的布局方向,可选值为horizontalvertical | string | horizontal |
fixed-bottom | 是否固定在窗口底部,可用做底部导航 | boolean | false |
color | 选项卡的背景颜色 | string | - |
active-color | 选项卡激活的主要颜色 | string | - |
inactive-color | 选项卡未激活的主要颜色 | string | - |
disabled-color | 选项卡禁用时的主要颜色 | string | - |
indicator-color | 选项卡激活指示器的颜色,未设置时回退为active-color | string | - |
indicator-size | 选项卡激活指示器的尺寸 | string | number | 2px |
elevation | 海拔高度,可选值为truefalse和0-24的等级,true时应用等级 4 | string | number | boolean | false |
sticky | 是否启用粘性布局 | boolean | false |
sticky-css-mode | 是否启用粘性布局的原生 css sticky 模式 | boolean | false |
sticky-z-index | 粘性布局的层级 | number | 10 |
safe-area | 是否开启底部安全区适配 | boolean | false |
offset-top | 吸顶距离 | string | number | 0 |
scrollable | 是否开启滚动,可选值为autoalways | string | auto |
indicator-position | 选项卡激活指示器的位置,可选值为normalreverse | string | normal |
Tab Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
name | 选项卡的名字 | string | number | - |
disabled | 是否禁用选项卡 | boolean | false |
ripple | 是否启用水波效果 | boolean | true |
TabsItems Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
v-model:active | 激活的选项卡的标识 | string | number | 0 |
can-swipe | 是否允许滑动切换 | boolean | true |
animated | 是否启用内容切换动画 | boolean | true |
loop | 是否允许循环切换 | boolean | false |
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 \| numberevent: Event |
插槽
| 组件 | 插槽名 | 说明 | 参数 |
|---|---|---|---|
Tabs | default | 选项卡组的内容 | - |
Tab | default | 选项卡内容 | - |
TabsItems | default | 视图组的内容 | - |
TabItem | default | 视图的内容 | - |
样式变量
以下为组件使用的 CSS 变量,可以使用StyleProvider组件进行样式定制。
Tabs Variables(tabs.less)
| 变量名 | 默认值 |
|---|---|
--tabs-item-horizontal-height | 44px |
--tabs-item-vertical-height | 66px |
--tabs-radius | 2px |
--tabs-padding | 12px |
--tabs-indicator-size | 2px |
--tabs-indicator-border-radius | 0 |
--tabs-indicator-background | var(--color-primary) |
--tabs-background | #fff |
--tabs-indicator-inner-size | 100% |
Tab Variables(tab.less)
| 变量名 | 默认值 |
|---|---|
--tab-padding | 12px |
--tab-active-color | var(--color-primary) |
--tab-inactive-color | #646566 |
--tab-disabled-color | var(--color-text-disabled) |
--tab-font-size | var(--font-size-md) |
--tab-font-weight | 400 |
--tab-active-font-size | var(--font-size-md) |
--tab-active-font-weight | 400 |
需要区分的是:内联 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.
相关推荐
Varlet TabsItems 组件使用指南:实现选项卡与视图的滑动联动
Varlet TabsItems 组件使用指南:实现选项卡与视图的滑动联动 本文以 Varlet 中 TabsItems (选项卡视图组)组件的官方 API 文
UI组件前端Varlet TabsItems 视图组深度解析:选项卡组与内容视图的双向联动实现
Varlet TabsItems 视图组深度解析:选项卡组与内容视图的双向联动实现 在 Varlet( @varlet/ui )的选项卡体系中, Tabs /
UI组件前端wired-tabs 手绘风格选项卡组件实战指南:基于 wired-elements 的 Tabs 使用与实现原理
wired tabs 手绘风格选项卡组件实战指南:基于 wired elements 的 Tabs 使用与实现原理 <wired tabs 是 wired el
UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考