小程序自带的tabBar虽然配置简单,但样式基本靠系统默认样式撑着,图标从灰色变成高亮色的那一瞬间没有任何过渡效果,切换起来总有种生硬的跳跃感。如果产品对视觉细节有要求,比如希望图标被选中时有一个轻微的放大回弹、文字颜色渐变、徽标数字弹跳出现,就必须走自定义tabBar的路线,用CSS过渡动画把这些细节做扎实。这篇文章从配置开启到组件编写,再到状态同步的坑,完整梳理一遍实现思路。

一、开启自定义tabBar的正确配置
微信小程序从基础库2.5.0开始支持自定义tabBar,做法是在app.json的tabBar配置中加一个"custom": true字段。需要注意的是,即使启用了自定义tabBar,list数组里的页面配置也不能删掉,因为页面路由仍然依赖这份配置来判断哪些页面属于tab页,系统只是把渲染权交给你自己。
配置开启后,还要在代码根目录下新建一个custom-tab-bar文件夹(目录名必须一模一样,不能写成custom-tabbar),里面放index.js、index.json、index.wxml和index.wxss四个文件,相当于一个特殊的全局组件。看一下基础配置:
{
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#07c160",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/logs/logs",
"text": "日志"
}
]
}
}这份配置里的颜色字段在自定义模式下不会被直接渲染,但保留它们没有坏处,某些异常兜底场景下系统仍会读取这些值。真正决定视觉效果的是custom-tab-bar组件里的wxml和wxss。
二、编写tabbar组件并实现选中态过渡动画
组件的js部分核心工作是维护一个selected字段记录当前选中的索引,并提供一个setSelected方法供各页面调用。先看js和wxml:
// custom-tab-bar/index.js
Component({
data: {
selected: 0,
color: '#999999',
selectedColor: '#07c160',
list: [
{ pagePath: '/pages/index/index', text: '首页', icon: 'home', badge: 0 },
{ pagePath: '/pages/logs/logs', text: '日志', icon: 'log', badge: 3 }
]
},
methods: {
setSelected(index) {
if (this.data.selected === index) return
this.setData({ selected: index })
}
}
})
<view class="tab-bar">
<view
wx:for="{{list}}"
wx:key="pagePath"
class="tab-item"
data-path="{{item.pagePath}}"
data-index="{{index}}"
bindtap="switchTab"
>
<view class="icon-wrap">
<image class="icon {{selected === index ? 'active' : ''}}" src="{{selected === index ? item.iconActive : item.icon}}"/>
<view wx:if="{{item.badge > 0}}" class="badge">{{item.badge}}</view>
</view>
<text class="label {{selected === index ? 'active' : ''}}">{{item.text}}</text>
<!-- 点击切换页面 -->
</view>
</view>点击切换的逻辑要在wxml绑定一个switchTab事件,在js里通过wx.switchTab跳转,这里不再赘述。真正让切换"活起来"的是wxss里的过渡动画。关键技巧是:不要用两个图标做显示隐藏切换,而是给同一个元素切换class,让transform和opacity通过transition自然过渡:
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 8rpx 0 4rpx;
}
.icon-wrap {
position: relative;
width: 52rpx;
height: 52rpx;
}
.icon {
width: 100%;
height: 100%;
opacity: 0.55;
transform: scale(0.9);
transition: transform 0.25s ease, opacity 0.25s ease;
}
.icon.active {
opacity: 1;
transform: scale(1.06);
}
.label {
font-size: 22rpx;
color: #999999;
transition: color 0.25s ease;
}
.label.active {
color: #07c160;
font-weight: 600;
}
.badge {
position: absolute;
top: -10rpx;
right: -16rpx;
min-width: 30rpx;
height: 30rpx;
line-height: 30rpx;
border-radius: 15rpx;
background: #fa5151;
color: #fff;
font-size: 20rpx;
text-align: center;
animation: badgePop 0.3s ease;
}
@keyframes badgePop {
0% { transform: scale(0); }
60% { transform: scale(1.25); }
100% { transform: scale(1); }
}这段样式里做了三层动效:图标透明度和缩放同时过渡,视觉上是从灰色淡出到高亮并轻微放大;文字颜色平滑渐变;徽标用@keyframes做一个先放大再回落的弹跳。如果想要更明显的回弹手感,可以把transition的timing-function换成cubic-bezier(0.34, 1.56, 0.64, 1),这是常用的过冲曲线,图标会先超过目标大小再缩回来,动画感更强。另外要注意,改图标src属于资源替换,不会有过渡效果,所以选中态的视觉变化尽量靠transform和opacity表达,颜色变化交给文字和遮罩层完成。
三、页面切换时的状态同步:最容易踩的坑
自定义tabBar有一个经典问题:每个tab页面都会实例化一份独立的tabbar组件。用户从首页切到日志页时,日志页拿到的是自己那份初始状态为0的组件,如果不在页面里主动同步,图标就会停留在错误的高亮位置,动画再好看也白搭。
解决办法是在每个tab页面的onShow生命周期里获取组件实例并调用setSelected:
// pages/logs/logs.js
Page({
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setSelected(1)
}
}
})这里有两个细节值得注意。第一,getTabBar()只在tab页里存在,普通页面调用会报错,所以加一个函数存在性判断更稳妥。第二,把同步逻辑写在onShow而不是onLoad,因为tab页第一次加载后会被缓存,之后每次切回来只会触发onShow,写在onLoad里的话第二次切换就不会更新状态了。
还有一个体验层面的优化点:由于每次切换都会执行setData,组件重新渲染时动画会重新播放,这正是我们想要的效果——切换瞬间图标从上一次的选中态过渡到新状态。但如果你的setSelected里做了其他数据更新(比如拉取最新徽标数),要避免频繁setData大对象,否则在低端机上动画可能掉帧。建议把动画相关的class切换和业务数据更新拆成两次轻量的setData调用,保证动画字段优先更新。
总结一下整体思路:配置层开启custom,组件层用class切换配合transition做视觉过渡,页面层在onShow里同步索引。三层各司其职,就能得到一个图标渐变放大、文字变色、徽标弹跳的丝滑tabBar,而且完全可控,后续想加凸起中间按钮或者角标动画,都只是在这个结构上继续扩展样式而已。