博客用的是 Hugo 的 PaperMod 主题,默认的导航栏在手机上是横向滚动的:菜单项一多就溢出屏幕,想看后面的得用手指拖着划,很难受。我希望把它改成常见的「点一下汉堡按钮弹出菜单」的形式,顺手还要让导航栏吸顶、点空白处能收回。这篇记录一下完整的改法。

PaperMod 默认的导航结构

先看主题的头部模板 themes/PaperMod/layouts/_partials/header.html,结构大概是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<header class="header">
    <nav class="header-nav">
        <div class="logo">
            <!-- 站点名 -->
            <div class="logo-switches">
                <!-- 主题切换按钮、语言切换 -->
            </div>
        </div>
        <ul id="menu" class="menu">
            <!-- 菜单项:range site.Menus.main -->
        </ul>
    </nav>
</header>

窄屏时 PaperMod 的处理方式是让 .menu 内部横向滚动:

1
2
3
@media (max-width: 900px) {
    .menu { overflow-x: auto; white-space: nowrap; }
}

这就是「要拖动」的根源。PaperMod 没有内置汉堡菜单。

改法:覆盖头部模板,加一个 checkbox

不想直接改主题文件(升级主题会被覆盖),所以把主题的 _partials/header.html 复制到站点的 layouts/partials/header.html 同名覆盖(Hugo 站点目录优先于主题目录),然后在原结构上动三处:

顺带弄清了一个约定:主题把自带的 header/footer 等模板放在 _partials/(下划线前缀表示"主题内部"),站点通常用标准目录 partials/。Hugo 会把两边的 partials/ 和 _partials/ 合并进同一个 partial 命名空间(调用时都不带目录前缀,直接 partial "header.html"),项目优先于主题——所以放在站点 partials/ 下的同名文件照样能盖掉主题 _partials/ 里的。只要站点内部两个目录别放同名文件就行。

  1. 在 .logo 之后插一个隐藏的 <input type="checkbox" id="menu-toggle">;
  2. 在 <ul class="menu"> 后面加一个 <label for="menu-toggle" class="menu-toggle">(画三条横线);
  3. 文件末尾加几行 JS,实现「点空白处收回」。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<header class="header">
    <nav class="header-nav">
        <div class="logo">…标题 + 主题切换…</div>

        <!-- ① 隐藏的开关:必须在 .menu 之前、与它同父级 -->
        <input type="checkbox" id="menu-toggle" class="menu-toggle-input" aria-hidden="true">

        <ul id="menu" class="menu">…菜单项…</ul>

        <!-- ② 汉堡按钮放最后:flex 布局自动推到最右端 -->
        <label for="menu-toggle" class="menu-toggle" aria-label="切换菜单" title="菜单">
            <span class="menu-toggle-bar"></span>
            <span class="menu-toggle-bar"></span>
            <span class="menu-toggle-bar"></span>
        </label>
    </nav>
</header>

这里有个很关键的坑:checkbox 必须和 .menu 是同一个父元素下的兄弟,CSS 才能用通用兄弟选择器 :checked ~ .menu 选中菜单。我第一次把它随手塞进 .logo-switches 里,展开规则怎么都不生效,折腾半天才发现是结构位置不对。

汉堡按钮的摆放也有讲究:放 .logo 里会挤在标题旁边,想要贴最右,就得放到菜单后面,space-between 布局会把它自动推到右端。

CSS:900px 以下折叠,点按钮展开

汉堡按钮、三横线的基础样式,以及折叠/展开规则都写在站点的 assets/css/extended/custom.css 里。extended 目录下的 CSS 会被 Hugo 自动合并到主题样式之后,天然拥有覆盖主题规则的优先级:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/* 隐藏的 checkbox:看不见,但状态可被 label 切换 */
.menu-toggle-input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

/* 汉堡按钮:宽屏默认不显示 */
.menu-toggle {
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 4px;
    width: 28px;
    height: 28px;
    padding: 5px;
    cursor: pointer;
    border: 1px solid var(--border);
    border-radius: 6px;
}

.menu-toggle .menu-toggle-bar {
    display: block;
    width: 100%;
    height: 2px;
    border-radius: 1px;
    background: var(--primary);
}

@media screen and (max-width: 900px) {
    /* 窄屏才显示汉堡按钮 */
    .menu-toggle {
        display: inline-flex;
        align-self: center;
    }

    /* 菜单默认收起;展开时作为悬浮面板,不撑高导航栏 */
    .header-nav .menu {
        display: none;
        position: absolute;
        top: calc(100% + 6px);
        right: var(--gap);
        width: max-content;
        min-width: 120px;
        max-width: calc(100% - var(--gap) * 2);
        z-index: 110;
        flex-direction: column;
        align-items: stretch;
        padding: calc(var(--gap) * 0.5) calc(var(--gap) * 0.8);
        background: var(--entry);
        border: 1px solid var(--border);
        border-radius: calc(var(--radius) - 2px);
        box-shadow: 0 10px 28px rgba(60, 55, 45, 0.18);
    }

    .header-nav .menu li {
        width: 100%;
    }

    /* 菜单项之间用虚线分隔,比实线轻 */
    .header-nav .menu li + li {
        border-top: 1px dashed var(--border);
    }

    .header-nav .menu li a {
        display: block;
        width: 100%;
        padding: calc(var(--gap) * 0.32) 0;
    }

    /* 勾选(点汉堡)后展开 */
    .menu-toggle-input:checked ~ .menu {
        display: flex;
    }
}

li + li 是「给除了第一个以外的每个菜单项顶上画条虚线」,比给每个 li 都画、再单独去掉第一个的写法干净。

为什么不用 JS 做展开?

常见的做法是 JS 切换 class。但这里其实只需要一个隐藏 checkbox 就能完成「点一下开、再点一下关」:label 点击会原生切换 checkbox 的 checked 状态,:checked 时 CSS 展开菜单,完全不依赖 JS,也没有多余的 class 管理。这个「checkbox hack」在移动端菜单、折叠面板里都很好用。

它做不到的只有一件事:点击菜单以外的空白处自动收回——checkbox 只能被自己的 label 切换,管不到页面其它位置的点击。这个需求就交给 JS 兜底,写在 header 模板末尾:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
    (function () {
        const input = document.getElementById('menu-toggle');
        if (!input) return;
        // 菜单已展开时,点击导航栏以外的任意位置即收回
        document.addEventListener('click', function (e) {
            if (input.checked && !e.target.closest('.header')) {
                input.checked = false;
            }
        });
    })();
</script>

e.target.closest('.header') 用来排除导航栏内部:如果点的是汉堡按钮(label),此时 checkbox 刚被切换,不该再被这个监听器改回去;点悬浮面板里的链接则直接跳转,页面刷新后状态自然复位。只监听「点在 .header 之外」的情况,就不会和按钮行为打架。

面板外观:几轮微调

菜单展开的形态也调了几版,值得记一下过程:

  1. 最初是文档流展开:菜单显示在导航栏下方第二行,会把导航栏撑高,视觉上是「导航栏长高了一块」,还和吸顶效果打架;
  2. 改成绝对定位悬浮:position: absolute 脱离文档流,面板悬在导航栏正下方 6px 处,盖在正文上方,导航栏高度稳定不跳动。配合 z-index: 110 保证盖住正文;
  3. 宽度贴内容:一开始面板左右拉满屏幕(left: 0; right: 0),手机上显得空旷;改固定 280px 又会在菜单项少时留白。最终用 width: max-content 让宽度刚好包住最长的菜单项,min-width: 120px 保证点击区域别太窄,max-width 兜底超窄屏不溢出;
  4. 位置靠右:面板 right: var(--gap),右缘和汉堡按钮对齐,从按钮正下方落下来,符合手指操作习惯。

配色上直接复用主题变量:background: var(--entry) 是比页面底色略亮的卡片色(我博客的纸张色体系里 entry 就是浅一档的纸色),--border 画边框,阴影用了偏暖的半透明黑 rgba(60, 55, 45, 0.18),浅色深色主题下都协调,无需为暗色单独写。

相关的小改动

这次顺带做了几处配套调整,一并记录:

  • 导航栏吸顶:默认导航栏滚出屏幕就没了。给 .header 加 position: sticky; top: 0; z-index: 100; background: var(--theme)。注意要给背景色,否则正文滚到导航栏下方会透出来;
  • 文章页侧栏避让:文章页右侧的目录/标签栏原本 sticky top: var(--gap),导航栏吸顶后会被挡住,改成 top: calc(var(--header-height) + var(--gap)),正好落在导航栏之下;
  • 移动端侧边卡片:文章页的分类/标签/系列卡片在窄屏原本 display: none,我把 HTML 移到正文之后、CSS 默认改为文档流展示(只有 .wide 时才绝对定位到右侧),手机端文章读完就能在底部看到这几张卡片。

涉及文件

文件作用
layouts/partials/header.html覆盖主题头部模板,加 checkbox、汉堡按钮、关闭脚本
assets/css/extended/custom.css汉堡按钮、折叠/悬浮面板、导航栏吸顶样式

断点选在 900px,和 PaperMod 自带的窄屏断点一致,超过 900px 一切恢复桌面横排。就这样,手机上终于不用再拖着导航栏滑了。