Bulma 固定导航栏实战:is-fixed-top/is-fixed-bottom 双步定位与 dropup 菜单的完整实现解析
Bulma 允许将 navbar 组件固定(fix)在页面顶部或底部,这一功能由两个修饰符类配合完成:给 nav 加 is-fixed-top 或 is-fixed-bottom,再给 <html> 或 <body> 加对应的 has-navbar-fixed-top / has-navbar-fixed-bottom 补偿页面内边距。对于底部固定导航栏,Bulma 还提供了配套的 dropup(向上弹出)菜单能力。本文基于仓库中的功能公告(docs/_posts/2017-11-01-fixed-navbar.md)、官方文档页与 SASS 源码,完整讲解固定导航栏的使用步骤、dropup 实现原理、触控/桌面细分变体、JS 动态切换方案,以及 E2E 测试对这些行为的具体断言,读完你可以直接在自己的项目中落地一个固定顶栏或底部导航栏,并理解其背后的 CSS 机制。
一、功能背景:fixed navbar 与 dropup 是一次发布的一对功能
仓库中的功能公告博文(2017-11-01,docs/_posts/2017-11-01-fixed-navbar.md)说明了这一特性最初发布时的定位:
- 可以把
navbar固定(fix)到页面顶部或底部; - 作为额外bonus,底部导航栏需要一个新的 dropup 功能——下拉菜单位于导航栏下方,对底部导航栏并不合适,因此同步实现了向上弹出的 dropup 菜单。
公告中给出的官方文档锚点(/documentation/components/navbar/#fixed-navbar)对应当前仓库中的文档页 docs/documentation/components/navbar.html,其中 “Fixed navbar” 小节将用法概括为一个两步流程(2-step process):
- 给
navbar组件添加is-fixed-top或is-fixed-bottom:
<nav class="navbar is-fixed-top">
</nav>
- 给
<html>或<body>元素添加对应的has-navbar-fixed-top或has-navbar-fixed-bottom,为页面提供合适的 padding,避免固定导航栏遮挡首屏/尾屏内容:
<html class="has-navbar-fixed-top">
</html>
第二步之所以必要,是因为 position: fixed 的元素会脱离文档流。从源码看,Bulma 的补偿方案非常直接——sass/components/navbar.scss 中:
html,
body {
&.#{iv.$class-prefix}has-navbar-fixed-top {
padding-top: cv.getVar("navbar-height");
}
&.#{iv.$class-prefix}has-navbar-fixed-bottom {
padding-bottom: cv.getVar("navbar-height");
}
}
即:把 html/body 的上(或下)内边距设为一个 navbar-height。该值由 Sass 变量 $navbar-height: 3.25rem 定义(sass/components/navbar.scss),并注册为 CSS 变量 --navbar-height,同时作用于 nav.navbar 的 min-height。若导航栏实际高度与 3.25rem 不同(例如自定义了更高的高度),需要同步调整该 CSS 变量,否则补偿的 padding 会不匹配。
二、一个完整的底部固定导航栏示例
官方文档页自带一个可交互的 “Show/Hide bottom navbar” 演示,其 DOM 结构来自示例包含文件 docs/_includes/docs/examples/navbar-bottom.html。去掉文档站特有的按钮逻辑后,核心结构如下:
<nav class="navbar is-fixed-bottom has-shadow">
<div class="navbar-brand">
<a class="navbar-item">Logo</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBottomExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBottomExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item">Home</a>
<!-- 底部导航栏使用 dropup:has-dropdown + has-dropdown-up -->
<div class="navbar-item has-dropdown has-dropdown-up is-hoverable">
<a class="navbar-link">Docs</a>
<div class="navbar-dropdown">
<a class="navbar-item">Overview</a>
<a class="navbar-item is-selected">Jobs</a>
<a class="navbar-item">Contact</a>
<hr class="navbar-divider">
<a class="navbar-item">Report an issue</a>
</div>
</div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary"><strong>Sign up</strong></a>
<a class="button is-light">Log in</a>
</div>
</div>
</div>
</div>
</nav>
配套的页面根元素:
<html class="has-navbar-fixed-bottom">
要点说明:
has-shadow:固定导航栏通常会压在滚动内容之上,加has-shadow可加一条细阴影增强层次。从源码看,is-fixed-bottom下阴影方向被专门翻转——$navbar-box-shadow-size: 0 0.125em 0 0(向下)在底部固定时改为0 -0.125em 0 0(向上),见 sass/components/navbar.scss;- dropup 修饰符
has-dropdown-up必须加在带has-dropdown的父级navbar-item上,而不是navbar-dropdown上,这是官方文档页 “Dropup” 小节的明确要求; - 移动端
navbar-burger的展开仍依赖 JS 切换is-active(Bulma 官方包不包含任何 JavaScript,文档页仅提供实现示例),这与固定定位无关,但底部导航栏同样需要这套逻辑。
三、dropup 菜单:has-dropdown-up 的 CSS 级实现
底部导航栏的菜单没有向下展开的空间,has-dropdown-up 通过一组定位与箭头翻转规则解决。核心实现位于 sass/components/navbar.scss(桌面断点内):
&.#{iv.$class-prefix}has-dropdown-up {
.#{iv.$class-prefix}navbar-link::after {
transform: rotate(135deg) translate(0.25em, -0.25em);
}
.#{iv.$class-prefix}navbar-dropdown {
border-bottom-color: cv.getVar("navbar-dropdown-border-color");
border-bottom-style: cv.getVar("navbar-dropdown-border-style");
border-bottom-width: cv.getVar("navbar-dropdown-border-width");
border-radius: cv.getVar("navbar-dropdown-radius")
cv.getVar("navbar-dropdown-radius") 0 0;
border-top: none;
bottom: 100%;
box-shadow: 0 -0.5em 0.5em hsla(..., 0.1);
top: auto;
}
}
对照默认 dropdown 的样式(sass/components/navbar.scss)可以看到 dropup 做了五处镜像调整:
| 属性 | 默认 dropdown | dropup(has-dropdown-up) |
|---|---|---|
| 垂直定位 | top: 100% |
bottom: 100%; top: auto; |
| 边框 | border-top(灰色顶边) |
border-bottom,border-top: none |
| 圆角 | 底部两角 border-end-*-radius |
border-radius: r r 0 0(顶部两角) |
| 阴影 | 0 0.5em 0.5em ...(向下) |
0 -0.5em 0.5em ...(向上) |
| 箭头 | 默认指向 | ::after 旋转 135deg 并平移,指向上方 |
使用示例(摘自文档页的 dropup 代码片段):
<div class="navbar-item has-dropdown has-dropdown-up is-hoverable">
<a class="navbar-link">
Docs
</a>
<div class="navbar-dropdown">
<a class="navbar-item">Overview</a>
</div>
</div>
展示方式与常规 navbar dropdown 一致:父级 navbar-item 加 is-hoverable(悬停展开)或 is-active(常驻展开/由 JS 控制)。注意 dropup 的定位与样式规则写在 @include mx.from($navbar-breakpoint)(即桌面端)内,而移动端下 navbar 的 dropdown 本来就是直接以块级形式内嵌展示,不涉及上下弹出问题。
四、固定定位的 CSS 机制:navbar-fixed mixin 与 z-index 层级
is-fixed-top / is-fixed-bottom 的共同规则来自私有 mixin(sass/components/navbar.scss):
@mixin navbar-fixed {
left: 0;
position: fixed;
right: 0;
z-index: cv.getVar("navbar-fixed-z");
}
随后按方向分别钉住上/下边缘(sass/components/navbar.scss):
&.is-fixed-bottom,
&.is-fixed-top {
@include navbar-fixed;
}
&.is-fixed-bottom {
bottom: 0;
&.has-shadow { box-shadow: ...; /* 向上阴影 */ }
}
&.is-fixed-top {
top: 0;
}
关键取值:
$navbar-z: 30是普通 navbar 的z-index(普通状态即为 relative 定位 + z-index 30);$navbar-fixed-z: 30是固定状态沿用的 z-index,因此固定与否不会改变其层级;- dropdown 的 z-index 为
$navbar-dropdown-z: 20,它处于 navbar 自身的层叠上下文内,所以不会被 navbar 外部、z-index 更高的元素意外遮挡时,需要自行评估页面中其他固定元素(如 header、modal)的层级关系。
仓库的 E2E 测试对以上行为做了明确断言,可视为该实现的验收基准。docs/cypress/e2e/components/navbar.spec.js 中:
it("has correct fixed Navbar", () => {
cy.get("#navbar-is-fixed-top").then(($) => {
const cs = window.getComputedStyle($[0]);
expect(cs.position).to.equal("fixed");
expect(cs.top).to.equal("0px");
expect(cs.zIndex).to.equal("30");
});
cy.get("#navbar-is-fixed-bottom").then(($) => {
const cs = window.getComputedStyle($[0]);
expect(cs.bottom).to.equal("0px");
expect(cs.position).to.equal("fixed");
expect(cs.zIndex).to.equal("30");
});
});
即固定导航栏的计算样式必须是 position: fixed、top: 0(或 bottom: 0)、z-index: 30;同一测试文件还断言了普通 navbar 的 min-height: 52px(3.25rem 在默认字号 16px 下的结果)。测试对应的 fixture 页面为 docs/cyp/components/navbar.html。
五、触控与桌面细分变体:-touch / -desktop 与 has-spaced-navbar
is-fixed-top / is-fixed-bottom 是全断点生效的定位。如果只想在某一类设备上固定,Bulma 提供了一组带设备后缀的变体,全部由同一 mixin 驱动:
- 触控端(
until($navbar-breakpoint),断点即$desktop):is-fixed-top-touch/is-fixed-bottom-touch定位 navbar;html/body上使用has-navbar-fixed-top-touch/has-navbar-fixed-bottom-touch补 padding;- 固定顶栏时,移动端展开的
navbar-menu被额外限制为max-height: calc(100vh - navbar-height); overflow: auto,保证菜单不会超出视口(sass/components/navbar.scss)。
- 桌面端(
from($navbar-breakpoint)):is-fixed-top-desktop/is-fixed-bottom-desktop定位 navbar;html/body上使用has-navbar-fixed-top-desktop/has-navbar-fixed-bottom-desktop补 padding。
桌面端还有两个专门配合 is-spaced 导航栏的变体(sass/components/navbar.scss):
&.#{iv.$class-prefix}has-spaced-navbar-fixed-top {
padding-top: calc(
#{cv.getVar("navbar-height")} + #{cv.getVar("navbar-padding-vertical")} * 2
);
}
因为 is-spaced 导航栏在 3.25rem 内容高度之外还有上下各 1rem($navbar-padding-vertical)的内边距,占位 padding 需要按 navbar-height + 2 × navbar-padding-vertical 补偿,避免顶部固定 + spaced 时仍遮挡内容。
另外,源码末尾还提供了一个与 hero 联动的组合修饰符(sass/components/navbar.scss):hero.is-fullheight-with-navbar 的 min-height 为 calc(100vh - navbar-height),用于在存在固定顶栏时让全屏 hero 不超出视口。
六、动态切换固定状态:文档站的参考实现
官方文档页(docs/documentation/components/navbar.html 中 “Try it out!” 部分)内置了一段 vanilla JS,演示如何运行时切换底部固定导航栏——这也是公告博文中 “Check it out!” 演示的对应实现:
<script>
document.addEventListener('DOMContentLoaded', () => {
const rootEl = document.documentElement;
const navbarBottomEl = document.getElementById('navbarBottom');
const fixBottomEl = document.getElementById('navbarFixBottom');
let fixedBottom = false;
fixBottomEl.addEventListener('click', () => {
fixedBottom = !fixedBottom;
if (fixedBottom) {
rootEl.classList.add('has-navbar-fixed-bottom');
navbarBottomEl.classList.remove('is-hidden');
} else {
rootEl.classList.remove('has-navbar-fixed-bottom');
navbarBottomEl.classList.add('is-hidden');
}
});
});
</script>
模式总结:切换固定状态 = 同步增删两处类名——html 上的 has-navbar-fixed-bottom 与 nav 上的 is-fixed-bottom(必要时配合 is-hidden 控制显隐)。缺少 padding 补偿那一侧时,就会出现首屏内容被遮挡的经典问题,这也是“两步流程”中第二步的意义所在。
同样,移动端 burger 菜单的展开由 JS 切换 navbar-burger 与 navbar-menu 上的 is-active 完成(文档页给出了 vanilla JS 与 jQuery 两种参考实现),该逻辑与固定定位正交,底部导航栏同样适用。
七、落地检查清单
综合文档与源码,实现固定导航栏时可对照以下清单:
nav.navbar添加is-fixed-top或is-fixed-bottom(需要全断点固定);仅针对单端时使用-touch/-desktop后缀变体;<html>或<body>添加has-navbar-fixed-top/has-navbar-fixed-bottom(或对应-touch/-desktop变体);导航栏带is-spaced时使用has-spaced-navbar-fixed-top/bottom(桌面端);- 底部导航栏的下拉菜单改为
has-dropdown has-dropdown-up,让菜单向上弹出; - 验证计算样式:
position: fixed、top: 0或bottom: 0、z-index: 30(与 docs/cypress/e2e/components/navbar.spec.js 的断言一致); - 若自定义了 navbar 高度,同步调整
--navbar-height,保证补偿 padding 与真实高度一致; - 检查页面上其他 fixed/sticky 元素(header、modal、toast)与 z-index 30 的相对关系,必要时提高 navbar 的
--navbar-fixed-z或降低遮挡元素层级。
以上所有类名与默认值均以当前仓库 SASS 源码(sass/components/navbar.scss)与预编译产物(css/bulma.css)为准;文档页与公告博文中引用的演示视频、截图属于站点资源,具体视觉表现以文档站渲染为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00