uni-app 原生的导航栏虽然省事,但稍微复杂一点的需求就捉襟见肘——搜索框居中、渐变背景、动态标题、右侧自定义按钮组合,这些在原配置项里都很难实现。更麻烦的是多端适配:Android 和 iOS 的状态栏高度不一样,H5 根本没有状态栏的概念,小程序的胶囊按钮还得留出安全距离。这篇文章会从零开始封装一个全局自定义导航栏组件,把上面这些坑逐个踩平。
一、为什么需要自定义导航栏
原生的 pages.json 里配置导航栏很方便,但它的能力边界非常清晰:标题只能居中,按钮最多左右各一个,样式只能改背景色和文字颜色。一旦产品经理要求”搜索框放在导航栏中间”、”导航栏背景做成品牌渐变色”、”右侧加两个图标按钮还要带角标”,你就只能关掉原生导航栏自己写了。
关掉原生导航栏很简单,在对应的页面配置里加一句 "navigationStyle": "custom"。但消失的不只是导航栏本身,系统帮你处理的状态栏占位也没有了——页面内容会从屏幕最顶部开始渲染,直接和状态栏区域重叠。这就是自定义导航栏需要解决的第一个问题:准确计算出状态栏和导航栏应该占据的高度,并把页面内容挤到安全区域里。
二、状态栏高度的多端差异
不同的端获取状态栏高度的方式完全不一样,这是适配的第一个门槛。
- App 端(Android 和 iOS):可以通过
uni.getSystemInfoSync().statusBarHeight获取,单位是 px。iOS 刘海屏通常在 44px 左右,非刘海屏 20px;Android 各家 ROM 差异很大,通常 24px 到 48px 不等。 - H5 端:没有状态栏概念,浏览器窗口顶部就是内容区,statusBarHeight 通常返回 0。但如果你在微信内嵌浏览器或某些 WebView 环境下,可能需要手动处理顶部安全距离。
- 小程序端:微信小程序可以通过
wx.getSystemInfoSync()拿到 statusBarHeight,但需要注意的是小程序的胶囊按钮(右上角的菜单)有自己的位置和尺寸,自定义导航栏必须避开它。
统一的状态栏高度获取逻辑可以封装成一个工具函数:
// utils/system.js
export function getStatusBarHeight() {
const systemInfo = uni.getSystemInfoSync();
let statusBarHeight = systemInfo.statusBarHeight || 0;
// H5 端特殊处理:如果拿不到状态栏高度且当前环境是移动浏览器
// #ifdef H5
if (!statusBarHeight && systemInfo.platform === 'android') {
statusBarHeight = 24; // 安卓默认状态栏高度
} else if (!statusBarHeight && systemInfo.platform === 'ios') {
statusBarHeight = 44; // iOS 默认(含刘海屏)
}
// #endif
return statusBarHeight;
}
这段代码用了 uni-app 的条件编译,#ifdef H5 包裹的代码只在 H5 平台编译,其他平台会自动忽略。这样所有平台统一调用 getStatusBarHeight() 就能拿到合理的值。
三、小程序胶囊按钮的安全距离
微信小程序的右上角有一个原生的胶囊按钮组(”…”和圆圈),自定义导航栏如果宽度延伸到这个区域,内容会被遮挡。正确的做法是计算胶囊按钮的位置,让导航栏内容区域自动避开。
获取胶囊按钮信息的方法:
// 仅在微信小程序平台有效
// #ifdef MP-WEIXIN
const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
// 返回值示例: { width: 87, height: 32, top: 48, right: 367, bottom: 80, left: 280 }
// #endif
胶囊的 top 值就是它距离屏幕上边缘的距离,height 是胶囊的高度。我们可以用这两个值反推导航栏的合理高度:
// 计算导航栏总高度(状态栏 + 导航栏内容区)
function getNavBarHeight() {
const statusBarHeight = getStatusBarHeight();
// #ifdef MP-WEIXIN
const menuButton = wx.getMenuButtonBoundingClientRect();
// 导航栏内容区高度 = 胶囊高度 + 上下间距
const navContentHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2;
return statusBarHeight + navContentHeight;
// #endif
// 非小程序平台:状态栏 + 固定导航栏内容高度(一般 44px)
return statusBarHeight + 44;
}
上面的计算逻辑里,menuButton.top - statusBarHeight 是胶囊按钮距离状态栏下边缘的间距,乘 2 之后作为导航栏内容区的高度,可以保证导航栏内容和胶囊按钮垂直居中对齐。这是微信官方推荐的做法。
四、封装全局自定义导航栏组件
把高度计算和布局封装成一个组件 custom-navbar.vue,可以在任意页面复用:
<template>
<view class="custom-navbar-wrap" :style="{ height: navBarHeight + 'px' }">
<!-- 状态栏占位 -->
<view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 导航栏内容 -->
<view class="navbar-content" :style="{ height: navContentHeight + 'px' }">
<view class="navbar-left">
<slot name="left">
<image v-if="showBack" src="/static/back.png" class="back-icon" @click="goBack"></image>
</slot>
</view>
<view class="navbar-center">
<slot name="center">
<text class="title-text">{{ title }}</text>
</slot>
</view>
<view class="navbar-right" :style="{ right: capsuleRightOffset + 'px' }">
<slot name="right"></slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'CustomNavbar',
props: {
title: { type: String, default: '' },
showBack: { type: Boolean, default: false },
},
data() {
return {
statusBarHeight: 0,
navContentHeight: 44,
capsuleRightOffset: 0,
};
},
computed: {
navBarHeight() {
return this.statusBarHeight + this.navContentHeight;
},
},
created() {
this.initNavBarInfo();
},
methods: {
initNavBarInfo() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
// #ifdef MP-WEIXIN
const menuButton = wx.getMenuButtonBoundingClientRect();
this.navContentHeight = menuButton.height + (menuButton.top - this.statusBarHeight) * 2;
// 胶囊距离右边缘的距离(屏幕宽度 - 胶囊右边距)
this.capsuleRightOffset = systemInfo.windowWidth - menuButton.right;
// #endif
// #ifndef MP-WEIXIN
this.navContentHeight = 44;
this.capsuleRightOffset = 10; // 非小程序留一点右边距
// #endif
},
goBack() {
uni.navigateBack();
},
},
};
</script>
这个组件的核心设计:
- 最外层容器的高度等于状态栏高度加上导航栏内容高度,撑开整个顶部安全区。
- 状态栏占位区透明,纯粹占高度。
- 导航栏内容区使用 flex 布局,左中右三栏,中间标题通过 flex 自动居中。
- 右侧区域的
right值动态计算,在小程序上精确避开胶囊按钮。 - 提供了
left、center、right三个插槽,不同页面可以按需定制内容。
五、在页面中使用
先在 pages.json 里关掉该页面的原生导航栏:
{
"path": "pages/home/home",
"style": {
"navigationStyle": "custom"
}
}
然后在页面里引入组件:
<template>
<view class="page-container">
<custom-navbar title="首页" :show-back="false">
<template #center>
<view class="search-bar">
<input type="text" placeholder="搜索商品" />
</view>
</template>
<template #right>
<view class="right-icons">
<image src="/static/icon-cart.png" class="icon"></image>
<image src="/static/icon-msg.png" class="icon"></image>
</view>
</template>
</custom-navbar>
<!-- 页面主体内容,加了 padding-top 避免被导航栏遮挡 -->
<view class="page-body" :style="{ paddingTop: navbarHeight + 'px' }">
<scroll-view scroll-y>
<!-- 实际业务内容 -->
</scroll-view>
</view>
</view>
</template>
<script>
import CustomNavbar from '@/components/custom-navbar.vue';
export default {
components: { CustomNavbar },
data() {
return {
navbarHeight: 0,
};
},
onLoad() {
// 计算导航栏总高度,给页面主体留出 padding-top
const systemInfo = uni.getSystemInfoSync();
const statusBarHeight = systemInfo.statusBarHeight || 0;
let navContentHeight = 44;
// #ifdef MP-WEIXIN
const menuButton = wx.getMenuButtonBoundingClientRect();
navContentHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2;
// #endif
this.navbarHeight = statusBarHeight + navContentHeight;
},
};
</script>
页面容器通过 paddingTop 把内容挤到导航栏下方,这个值直接从组件里的计算逻辑同步过来。不同页面可以各自获取,也可以进一步封装成 mixin 或 composable 函数来复用。
六、处理 H5 端的特殊安全区域
H5 环境里没有原生状态栏,但移动端浏览器有安全区域(刘海屏底部的横条、顶部的传感器区域)。CSS 的环境变量 env(safe-area-inset-top) 可以获取这个值。在自定义导航栏组件里补充一段针对 H5 的样式逻辑:
// 在组件的样式中增加对 H5 的支持
// 注意:以下样式应该放在组件的 style 标签里,这里仅为展示逻辑
.status-bar-placeholder {
// #ifdef H5
// H5 使用 constant 和 env 双保险
height: constant(safe-area-inset-top);
height: env(safe-area-inset-top);
// #endif
}
同时在计算逻辑里也把 H5 的情况纳入考虑:
// #ifdef H5
// 如果状态栏高度取不到,尝试使用 safe-area-inset-top
if (!this.statusBarHeight) {
const safeAreaTop = parseInt(getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)'), 10) || 0;
this.statusBarHeight = safeAreaTop;
}
// #endif
这样在微信内置浏览器、Safari 等支持安全区域变量的环境里,导航栏也能准确地避开刘海区域。
七、踩过的几个坑和对应解法
7.1 小程序里 fixed 定位的基准问题
自定义导航栏一般使用普通流布局或者 fixed 定位。如果使用 fixed 定位,需要考虑 z-index 层级,并且页面内容需要用等高的 padding-top 或顶层占位元素来避免被挡住。本文采用的是普通流布局配合外层占位的方式,避免了 fixed 带来的层级冲突。
7.2 Android 和 iOS 状态栏高度在暗黑模式下的变化
部分 Android 机型在切换暗黑模式时状态栏高度不变,但背景色和文字颜色需要适配。自定义导航栏的背景色可以用 CSS 变量控制,在 @media (prefers-color-scheme: dark) 里切换即可。
7.3 多端样式的像素精度差异
uni.getSystemInfoSync() 返回的高度单位是 px,但在不同设备上 px 对应的物理像素不一致。CSS 里写的高度直接用 px 值即可,uni-app 在编译时会按比例转换。唯一需要注意的是,自定义组件的高度数值应该直接从系统信息里获取,不要硬编码,否则在不同设备上会有错位。
7.4 返回按钮的逻辑差异
App 端和 H5 端的返回可以用 uni.navigateBack(),小程序端在页面栈深度为 1 时按返回会直接退出小程序,产品可能需要在这种情况下跳转到首页而不是退出。可以在组件的 goBack 方法里增加判断:
goBack() {
const pages = getCurrentPages();
if (pages.length <= 1) {
uni.switchTab({ url: '/pages/index/index' });
} else {
uni.navigateBack();
}
}
八、可以继续扩展的方向
这个组件只是一个基础骨架,根据业务需要可以继续扩展:
- 添加背景色渐变、毛玻璃效果等视觉定制属性。
- 支持自定义标题字体大小和颜色。
- 当页面滚动时动态改变导航栏透明度或阴影。
- 在右侧按钮区域封装更复杂的组合,比如购物车图标加数字角标。
- 适配更多平台(支付宝小程序、抖音小程序等)的胶囊按钮逻辑。
越是通用的组件,越需要在基础版之后根据实际场景逐步迭代,而不是一开始就做得很重。文中的代码已经涵盖了最核心的状态栏适配和胶囊按钮避让逻辑,直接拿去用基本不会有问题。
九、总结
自定义导航栏这件事,本质上是在和不同平台的状态栏机制打交道。uni-app 给你关掉了原生导航栏,但没有替你处理好状态栏的占位和多端差异。把状态栏高度计算、胶囊按钮位置获取、安全区域适配这三件事封装进一个组件之后,自定义导航栏就从”每次都踩坑”变成了”一次封装到处使用”。
文中所有代码都可以直接复制到 uni-app 项目里运行,唯一需要注意的是不同平台的编译差异——条件编译宏已经替你处理好了大部分分支,剩下就是根据自己的设计稿调整样式细节。动手跑一遍,比看文档十遍管用得多。

