微信小程序原生的tabbar虽然够用,但样式定制空间非常有限。当产品经理提出「消息数徽标要做一个翻牌效果,消失的时候要淡出」这种需求时,原生setTabBarBadge接口就无能为力了。好在小程序官方提供了custom-tab-bar机制,允许我们完全接管底部导航的渲染和交互,徽标的任何动效都可以自己写。本文就以数字翻转动画和渐隐渐现效果为例,完整讲一遍实现过程。

一、搭建自定义tabbar的基础结构
首先要明白custom-tab-bar不是一个普通组件,它是一个固定名称的目录,必须放在小程序根目录下(使用分包时不放在分包里)。在app.json中开启custom: true,同时仍然要声明tabbar的list配置,因为跳转逻辑和页面路径还依赖这份配置:
{
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#07c160",
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/message/message", "text": "消息" },
{ "pagePath": "pages/mine/mine", "text": "我的" }
]
}
}接着在根目录创建custom-tab-bar/index.js、index.json、index.wxml、index.wxss四个文件。index.json中要把component声明出来:
{
"component": true
}这里有一个非常经典的坑:每个tab页面都会持有自己的一份custom-tab-bar实例。也就是说,在首页改了徽标数字,切到消息页后徽标可能又变回旧值。解决办法是在每个tab页面的onShow里主动同步一次全局状态,把存在globalData或者状态管理里的未读数重新set进去。虽然写法上略繁琐,但这是目前最稳妥的多端同步方案。
二、数字翻转动画的实现思路
翻转动画的核心是把「数字变化」拆解成视觉上的「翻牌」过程。常见做法是:当数字从旧值变为新值时,让旧数字向上旋出,新数字从下方旋入,类似机场翻页显示屏的效果。由于CSS的rotateX是围绕横轴旋转,配合transform-style: preserve-3d和perspective就能做出立体的翻牌感。
在wxml中,徽标结构可以设计成两层:一层显示旧数字,一层显示新数字,两层叠放在一起:
<view class="badge-wrap" wx:if="{{badgeNum > 0}}">
<view class="flip-box {{flipping ? 'flipping' : ''}}">
<view class="flip-item old">{{oldNum}}</view>
<view class="flip-item new">{{newNum}}</view>
</view>
</view>对应的wxss中,关键在于backface-visibility: hidden,它让卡片翻转超过90度时正面消失、背面出现,两个面配合起来就是完整的翻转周期:
.badge-wrap {
perspective: 300rpx; /* 提供三维透视距离,数值越小翻转越夸张 */
}
.flip-box {
position: relative;
transform-style: preserve-3d;
transition: transform 0.4s ease;
}
.flip-box.flipping {
transform: rotateX(-180deg);
}
.flip-item {
position: absolute;
width: 32rpx;
height: 32rpx;
line-height: 32rpx;
text-align: center;
border-radius: 16rpx;
background: #fa5151;
color: #fff;
font-size: 20rpx;
backface-visibility: hidden;
}
.flip-item.new {
transform: rotateX(180deg); /* 新数字预先翻到背面藏起来 */
}JS侧的逻辑是:监听到数字变化时,先记录旧值,设置新值,加flipping类触发过渡,等400毫秒动画结束后移除类并复位。有一点要特别注意,复位时如果直接去掉transform,会产生反向动画的视觉抖动,可以临时关掉transition再复位,或者在复位前用nextTick配合一个visibility控制。另外当数字超过99时,业界惯例是显示99+,此时文字变长,记得给徽标改成自适应宽度的胶囊形状,用padding: 0 8rpx撑开即可。
三、渐隐渐现效果的两种实现方案
渐隐渐现比翻转简单,但也有讲究。第一种方案是纯CSS过渡,给徽标容器加opacity和scale的transition,数字从0变到正数时插入节点自然会从初始状态过渡到目标状态。不过这里有个小程序特有的问题:wx:if控制的节点插入时,transition不会自动触发,因为节点渲染和样式应用是同一帧完成的。解决办法是插入后延迟一帧再切换class,利用wx.nextTick或者setTimeout(fn, 20):
// 数字从0变为正数,徽标淡入出现
this.setData({ badgeNum: num, badgeVisible: true }, () => {
wx.nextTick(() => {
this.setData({ badgeAnimated: true });
});
});
.badge-wrap {
opacity: 0;
transform: scale(0.3);
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.badge-wrap.show {
opacity: 1;
transform: scale(1);
}第二种方案是用WXSS的animation关键帧动画。区别在于transition需要状态变化来触发,而animation是节点渲染后自动播放的,更适合「出现一次就完事」的场景。渐隐消失时配合scale缩小加轻微上移,观感会比单纯的opacity变化精致不少。上面的cubic-bezier(0.34, 1.56, 0.64, 1)是一个带回弹的贝塞尔曲线,徽标弹出时会有Q弹的过冲效果,比线性动画生动很多。
两种方案的取舍:如果徽标状态频繁切换,用transition更可控;如果是一次性的引导动画,用animation代码更少。另外建议把动画时长控制在300到400毫秒之间,太快用户看不清,太慢会拖累操作流畅感,尤其tab切换本身就带页面切换动画,徽标动效太长会显得整体迟钝。
四、常见坑点与优化建议
第一个坑是层级问题。custom-tab-bar是固定在页面底部的原生层级外组件,但在部分安卓机型上,如果页面内有cover-view或弹窗试图盖住tabbar区域,可能出现渲染错乱。徽标本身在tabbar内部不受影响,但要避免让页面元素延伸到tabbar高度范围内。
第二个坑是频繁setData带来的性能开销。消息推送密集时,如果每来一条消息就立即播放动画,会出现动画还没翻完数字又变了的情况。建议在JS侧做一个简单的节流:300毫秒内的多次数字变更合并为一次动画,旧值直接取上一次动画结束后的值。同时setData时只传徽标相关的字段,不要把整个data对象扔进去,减少通信成本。
第三个是可访问性提醒:纯动效的徽标对无障碍不友好,可以在徽标节点上加上aria-label属性,读作「N条未读消息」,视障用户借助屏幕阅读器也能感知到提醒。最后,做完整套动效后记得在开发者工具的「真机调试」里过一遍iOS和安卓,因为backface-visibility在个别老版本安卓WebView上支持不完整,必要时可以降级为纯opacity切换,保证功能可用性优先于动效表现。