news 2026/10/17 2:46:46

纯CSS实现导航栏凸起效果:伪元素与圆角实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯CSS实现导航栏凸起效果:伪元素与圆角实战

1. 项目效果与设计思路拆解

1.1 凸起导航栏到底是个什么效果

先把这个项目标题翻译成大白话:导航栏练习,凸起导航栏效果。意思就是在一个常规的横向导航栏里,让当前选中项或者鼠标悬停的某一项,在视觉上“鼓起来”“顶起来”,产生一种超出导航条平面的立体感,而不是传统那种单纯变个文字颜色、加个下划线或者换个背景色的扁平反馈。

我第一次看到这类效果是在不少移动端 App 的底部 Tab 栏里:中间那个“发布”按钮做成一个悬浮的圆形凸起,明显高于左右两边的普通图标,看起来就像从底栏里“长”出来一样。PC 端则常见于顶部导航,鼠标扫过某菜单项时,那一项的底边带一个弧形拱起,或者整个项目向上鼓起一小截,配合一块阴影,像真的从面板上弹起来。

把它当成一个练习项目,我觉得特别合适。它不依赖任何框架和 JavaScript,只靠 HTML 加 CSS 就能做出来,写完以后你会发现自己对定位、伪元素、圆角、层叠上下文、过渡动画这些基础概念的理解都会有明显提升。而且这套思路稍加变形,就能迁移到按钮、卡片、轮播图指示器、分页器这些常见组件上,性价比很高。

1.2 为什么值得单独拎出来练习

很多前端初学者会有一个误区:觉得导航栏就是 flex 布局加一排<a>标签,无非调调颜色、加加 hover。真正做凸起效果的时候才会发现,难点根本不在布局,而在“凸起”这个视觉差是怎么制造出来的。

要做出真实的凸起感,至少要解决四件事:第一,凸起的部分不能破坏导航条的背景和整体结构;第二,凸起的轮廓要圆润自然,不能出现生硬的直角;第三,凸起区域要独立叠在导航条之上,但又不影响周边元素的点击;第四,切换选中态时最好带一点过渡动画,不然会显得很生硬。

这些问题串起来,正好覆盖了 CSS 里几个高频考点:position: relative/absolute的配合、::before/::after伪元素的灵活运用、border-radius如何构造弧形、z-index和层叠上下文的坑、以及transition的细节。项目规模不大,但知识点密度很高,做一遍比看十篇零散教程都管用。

2. 技术选型与实现方案对比

2.1 三种常见实现路径

动手之前,我先把网络上和我自己经验里的主流方案过了一遍,大致分成三类。

第一类是transform: translateY()配box-shadow的做法。思路很简单:导航项默认在导航条内部,hover 或选中时把该项整体向上平移几个像素,同时加一层向下的投影。这种方案最省事,但问题是它只能实现“整块浮起”,边缘轮廓依然是方方正正的矩形,缺少那种从导航条里“长出来”的弧形过渡,真实感比较弱。

![说明:该方案效果示意,顶起平直无明显弧线]

第二类是用内外两层背景去模拟。也就是说,导航条背景是一个颜色,某个导航项的背景用另一个颜色,再在它的底部通过边框圆角制造一个圆弧缺口,让背景色漏出来形成拱形。这种方案效果很接近目标,但代码写起来容易混乱,尤其涉及多级菜单或者容器带圆角时,几个背景色之间互相覆盖,调起来很痛苦。

第三类是我最后采用的方案:利用伪元素制造凸起轮廓。核心思路是给当前激活的导航项加一个::before或者::after伪元素,这个伪元素的高度比导航项本身大出一截,底部用大半径border-radius撑出弧形,再让这个伪元素的背景色跟导航项自身背景保持一致,最后通过z-index让伪元素盖在导航条上面。这样从视觉上看,就像这块区域真的从导航条面板里拱起来了。

2.2 我为什么选择“伪元素圆角”路线

选第三条路线,最重要的原因是它的“侵入性”最小。它不需要改变导航条的背景结构,也不会给真实导航项增加额外的盒子,所有凸起的视觉欺骗都交给伪元素完成,后续想改成平直样式,直接把伪元素删掉或者把高度改成 0 就行。

还有一个工程上的好处:灵活度高。这个方案里,凸起的高度、弧形的宽度、左右各留多少边距,全部由伪元素的尺寸和圆角参数决定。你可以轻松做出“半圆拱”“微凸 4px”“大弧度浮起”等不同性格的效果,而不用像第二类方案那样去重新设计背景分层。

我当时在本地实验过第一类和第三类方案的对比:第一类在浅色背景下,顶起的矩形边缘和阴影之间总有一条细缝,不仔细看还行,放大看就露馅;第三类只要把伪元素背景色与导航项对齐,边缘完全无缝,弧形过渡非常自然。所以最终就定下来走伪元素路线。

3. 核心代码实现与逐步拆解

3.1 HTML 结构:最小可复现结构

先说 HTML。整份代码不需要任何 JavaScript,我给这个练习准备的最小结构是这样的:

<nav class="nav-wrap"> <div class="nav"> <a href="#" class="nav-item">首页</a> <a href="#" class="nav-item active">分类</a> <a href="#" class="nav-item">发现</a> <a href="#" class="nav-item">我的</a> </div> </nav>

结构上就是经典的nav > a列表。外层nav-wrap负责整体的宽度和居中,可以理解成导航条的“外框”;内层nav是实际承载导航项的容器,背景色和圆角都放在这一层;每个.nav-item是真正的导航项。当前选中项通过.active类来标记,你完全可以把它换成 Angular 或 Vue 里的router-link-active等框架自带类名。

有一点要提醒:如果你希望 hover 也要有凸起效果,那就把主要样式同时写在.nav-item:hover和.nav-item.active上,两个选择器共用一个样式规则,既避免重复代码,也不会遗漏交互状态。

3.2 CSS 基础样式:从扁平到凸起

CSS 部分我按顺序拆开讲。第一步,先把导航条整体铺好:

.nav-wrap { width: 600px; margin: 60px auto; font-size: 16px; } .nav { display: flex; align-items: stretch; background: #f4f6f8; border-radius: 12px; padding: 10px 16px; }

这里有一个关键点:.nav的背景色我特意选了浅灰,而导航项本身默认不设背景色,这样导航条整体是完整的浅灰面板。凸起效果只发生在特定的导航项上,所以下一步让它“长出”自己的背景和弧度。

.nav-item { position: relative; flex: 1; display: flex; align-items: center; justify-content: center; padding: 14px 0; color: #555; text-decoration: none; transition: color 0.25s ease; } .nav-item:hover { color: #222; } .nav-item.active { color: #ff5a5f; }

注意.nav-item上的position: relative非常重要。没有它,后面伪元素::before的position: absolute就只能相对更上层的某个定位祖先去定位,你就很难精确控制凸起的位置了。

接下来是重头戏,凸起的伪元素:

.nav-item::before { content: ""; position: absolute; left: 50%; bottom: -10px; transform: translateX(-50%); width: 60%; height: 18px; background: #ffffff; border: 1px solid #e2e2e2; border-bottom: none; border-radius: 10px 10px 0 0; opacity: 0; transition: opacity 0.25s ease; } .nav-item.active::before, .nav-item:hover::before { opacity: 1; }

说几个必须理解的参数。bottom: -10px表示伪元素从导航项的底边向下伸出 10px,这 10px 就是凸起超过导航条底边的部分。left: 50%配合transform: translateX(-50%)是把伪元素水平居中,让它始终对齐导航项中心。width: 60%让凸起的宽度小于导航项本身,两边留出空隙,这样更像一个从面板上拱起来的鼓包。

border-radius: 10px 10px 0 0是弧形的来源:只保留左上角和右上角的圆角,右下角和左下角为 0,这样顶部是圆的、底部是平的。画一个草图你就明白了,这个伪元素本质上是一个“上圆下方”的白色小矩形,底边伸出导航条之外,中间这段底部就会形成一个圆弧状的凸起轮廓,被浅灰背景一衬托,立体感就出来了。

伪元素默认opacity: 0,只有在 hover 或者 active 时才显示。用透明度而不是直接控制显示隐藏,是为了让凸起有一个淡入过程,不至于突然蹦出来吓人一跳。

3.3 动画过渡细节

如果你以为上面就够了,那还差一步。直接把opacity从 0 切到 1 只是“淡入”,并没有真正的“拱起来”的位移感。我实际做的时候,在最终代码里给伪元素加了两个方向的过渡参数:

.nav-item::before { bottom: -8px; height: 12px; transition: opacity 0.25s ease, bottom 0.25s ease, height 0.25s ease; } .nav-item.active::before, .nav-item:hover::before { bottom: -12px; height: 20px; opacity: 1; }

这样当鼠标悬停,伪元素一边淡入,一边从bottom: -8px, height: 12px过渡到bottom: -12px, height: 20px。视觉上,凸起会有一个从“略微鼓出”到“完全顶起”的动态过程,比单纯透明度变化更有生命力。高度差只有 8px,动起来却很自然,不会让人觉得夸张。

这里要顺带提一个常见误区:很多人会想用transform: translateY()来做这个位移,但transform和transition配合时,变化的是整个伪元素的平面位置,轮廓的弧度并不会跟着“生长”,效果远不如动态修改height和bottom来得细腻。所以我在这个练习里刻意放弃 transform 位移,改用尺寸变化来控制视觉生长。

4. 进阶变体:移动端悬浮式凸起 Tab 栏

4.1 悬浮圆钮的定位思路

做完基础版之后,我想再分享一个从它衍生出来的进阶变体,也是我在实际接项目时被问过好几次的场景:移动端底部的悬浮式凸起 Tab 栏,类似电商 App 中间“发布”按钮那种效果。

HTML 结构其实差不多,差异主要在目标导航项上:

<nav class="tab-bar"> <a href="#" class="tab-item">首页</a> <a href="#" class="tab-item">消息</a> <a href="#" class="tab-item tab-item--main">发布</a> <a href="#" class="tab-item">购物车</a> <a href="#" class="tab-item">我的</a> </nav>

中间那个.tab-item--main就是需要凸起的悬浮按钮。它的核心样式:

.tab-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; background: #ffffff; border-top: 1px solid #eaeaea; padding: 8px 0 10px; } .tab-item--main { position: relative; margin-top: -22px; }

关键动作就是margin-top: -22px,把中间这个项整体往上顶 22px。然后配合伪元素画一个大圆:

.tab-item--main::before { content: ""; position: absolute; left: 50%; top: -18px; transform: translateX(-50%); width: 56px; height: 56px; background: #ff5a5f; border-radius: 50%; box-shadow: 0 4px 12px rgba(255, 90, 95, 0.35); }

这样中间按钮就能“浮”在底栏上方,超出底栏的高度由margin-top和伪元素向上位移共同决定。阴影的存在会让悬浮感更强烈,但注意阴影方向一定要朝上,因为光源通常在上方,阴影落在物体下方和底部面板上,才是合理的视觉逻辑。

4.2 凹陷容错处理

这个变体在真实项目里有个隐藏的坑:如果底栏本身带了半透明背景或者毛玻璃效果,中间按钮向上顶出之后,它底部和底栏衔接的区域容易出现一条不自然的缝隙,看起来像按钮是“贴”上去而不是 “长”出来的。

我的处理办法是:把按钮伪元素的圆角方向朝向底栏,并且在按钮底部加一层与底栏背景色相同的渐变过渡,或者直接把按钮做成一个“中间向上凸、两侧逐渐贴近底栏”的胶囊形。简单说,就是让凸起物在视觉上和底栏融为一体,而不是干干净净地悬空。

如果底栏背景是纯色,还有一个偷懒但有效的方式:给中间按钮伪元素再加一个和底栏同色的::after,放在按钮底部,挡住交界的边缘线,这样基本无痕。但这个方法只适合纯色背景,背景一旦带纹理或者图片就失效了,到时候还是得老老实实调渐变。

5. 常见问题与排查实录

5.1 伪元素超出容器被截断

我在做练习的时候第一个遇到的坑就是:伪元素明明设了bottom: -10px,结果怎么调都不见凸起,打开浏览器调试面板才发现伪元素直接被裁掉了。原因是.nav容器上设了overflow: hidden,或者某些组件库的全局样式给容器加了border-radius连带触发了裁剪。

排查方法很简单:在浏览器开发者工具里选中伪元素,看它的盒子模型区域是否完整显示。如果显示但视觉上被切掉一半,十有八九就是容器裁剪。解决方案是调整容器,让它不要裁剪子元素,或者把凸起设计成完全在导航条内部实现的“内凸”效果,而不依赖超出容器。

5.2 相邻导航项遮挡凸起区域

第二个典型问题是:凸起伪元素虽然正常渲染了,但旁边导航项的文字或者背景把它盖掉了一部分。这本质上是层叠顺序的问题。伪元素默认参与普通文档流内的层叠,它在同一层的导航项后面出现,但如果相邻项后来设了position: relative且没有指定z-index,就可能产生层级反复。

处理方式是给伪元素主动加一个z-index: 1,如果还不能解决,就把整个导航项设为z-index: 2,总之上声场里一定要闭合好。

.nav-item.active { position: relative; z-index: 2; } .nav-item.active::before { z-index: -1; }

注意:我用z-index: -1让伪元素躲在导航项内容的后面,但又能盖住导航条顶部,这样当凸起区域和相邻项叠在一起时,主体文字清晰可点,凸起的背景又不会被吞掉。

5.3 hover 区域太小、误触

还有一次我在实际项目里用这个效果,发现有用户反馈“鼠标想点分类,结果把发现也触发了”。排查后发现问题在凸起宽度:我把width设成了 40%,导致可 hover 区域集中在导航项中心,两边各留了 30% 的间隙,这些间隙一悬停就落到相邻导航项的触发范围里。

要解决它非常简单:把伪元素的宽度调大,贴近或覆盖导航项的整个宽度,只留很小的左右边距。比如:

.nav-item::before { width: 90%; }

这样凸起视觉上依旧是一个鼓包,但用户把鼠标移动到导航项边缘时,也能稳定命中当前项,不会产生误触。如果你是给移动端用的,也要注意把伪元素的可点击区域覆盖整个 tab 项,而不是只做视觉凸起。

5.4 圆角过渡不平滑

最后一个想说的是圆角参数。很多初学者喜欢把border-radius设成 50% 或者很大的固定值,结果凸起部分变成一个像馒头一样的半圆,和导航栏本身的气质完全不搭。实际上,凸起弧度应该和导航条的整体圆角保持一致或略小一点。

比如导航条本身是border-radius: 12px,我的凸起伪元素一般取10px左右。当导航条是直角风格时,凸起部分也不宜做得太圆,否则视觉风格会打架。多调试几次,让弧度和周围元素融在一起,而不是单独追求“圆”。

这里我整理了一张常见问题速查表,方便你边做边对着排查:

问题现象可能原因处理办法
凸起部分显示不出来容器overflow: hidden裁剪去掉裁剪或改用内部凸起思路
凸起被相邻项盖住层叠上下文顺序问题给目标项加z-index,伪元素改负层级
hover 误触发相邻项凸起宽度过窄、热区太小调大伪元素宽度到 80%~95%
凸起形状像馒头圆角参数不协调让圆角跟整体风格一致,小弧度为主
过渡效果生硬只切了透明度没做尺寸变化同步设置height和bottom的过渡

6. 实操心得与扩展建议

6.1 真实场景中的取名与组件化建议

在这个练习完成后,我最大的一个心得是:不要把凸起逻辑写死在具体类名上。建议把“是否凸起”“凸起的弧度”“凸起的高度”等做成 CSS 变量:

.nav-item { --bump-width: 60%; --bump-height: 18px; --bump-bottom: -10px; --bump-radius: 10px; } .nav-item::before { width: var(--bump-width); height: var(--bump-height); bottom: var(--bump-bottom); border-radius: var(--bump-radius) var(--bump-radius) 0 0; }

这样做的好处是:以后如果你要用在多个风格不同的场景里,只需要在特定容器上重新定义这几个变量即可,不必复制整段 CSS。我在一个组件库项目里就是这么做的,把导航项做成可配置组件,颜色、圆角、凸起幅度全部由外部传入,维护成本一下子降了下来。

6.2 后续可扩展的方向

这个效果做完之后,我还顺手把它迁移到了另外几个场景里,发现效果都不错,推荐你也试试。

第一个是分页器。当前页码用一个凸起的圆形背景标出来,比传统的纯色块更有层次感,尤其适合轻量级的列表页面。第二个是步骤条。在步骤条里,当前步骤下面的圆点可以做成大一号的凸起,配合一条细线进度条,整个流程的“当前位置”非常醒目。第三个是卡片式页签。在内容卡片的顶部 Tab 区域,把选中项做成向上微微凸起的卡片头,同时盖住卡片的上边框,视觉上就像这张卡片真的被抽出来了一样。

如果时间充裕,你还可以在这个项目基础上,试着加上暗色模式适配。很多浅色背景方案切到深色背景后,凸起边界会变得很突兀,主要因为阴影和背景色对比关系变了。改成深色后,需要把伪元素背景色跟着切换,并降低阴影的不透明度,才能保持同样的立体感。

6.3 最后分享一个小技巧

按照我个人的实操习惯,每写完一个这种视觉类小效果,我都会在浏览器里用printBackground截图,然后放到移动端和桌面端的模拟器里分别看一遍。很多在桌面端觉得自然的弧度和阴影,到了移动端会因为设备像素比不同、背景色压缩渲染等原因发生变化。凸起类型的视觉最容易在色彩过渡和阴影边缘上“翻车”,多看几台设备,比反复改代码更高效。

这个“导航栏凸起”练习,表面上看只是做了个视觉样式,其实把 CSS 中最常用的几大核心机制都串了一遍。做的时候多调试几次参数,你会收获比预期更多的经验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 2:45:31

MFC对话框集成SQLite:从C API封装到CListCtrl刷新的完整实践

简介&#xff1a;这是一份面向C/Windows开发者的MFC对话框程序集成SQLite3完整示例&#xff0c;适合刚开始在桌面应用中引入嵌入式数据库的初学者参考。压缩包共38个文件&#xff0c;主要包含8个h头文件与4个cpp源码文件、sqlite3的dll和lib库、exe可执行程序、db数据库文件&am…

作者头像 李华
网站建设 2026/10/17 2:45:16

【AI大模型】Checkpoint 保存策略:训练到一半断电了也不怕

【AI大模型】Checkpoint 保存策略:训练到一半断电了也不怕 写在前面:训练是长跑,随时能“存进度”才安心 大模型微调常常要跑几个小时甚至几天。最怕的就是训练到一半断电、断网、显存 OOM,进度全部清零。解决办法就是 Checkpoint(检查点)——定时把训练进度存盘,断了…

作者头像 李华
网站建设 2026/10/17 2:44:53

56.实战PostgreSQL-pgvector-FastAPI构建轻量级RAG

实战&#xff1a;PostgreSQL pgvector FastAPI 构建轻量级 RAG 码海寻道 大模型、智能体与 RAG 工程组件系列第 56 篇 本篇用一个轻量方案串起 RAG 最小闭环&#xff1a;PostgreSQL 保存 Chunk 和业务元数据&#xff0c;pgvector 保存向量&#xff0c;FastAPI 提供检索接口。…

作者头像 李华
网站建设 2026/10/17 2:44:28

Python 闭包:为什么函数能记住早就销毁的变量

我感觉的闭包 刚学 Python 的时候&#xff0c;跟着课程走&#xff0c;感觉 Python 就那样也不难啊&#xff0c;直到我碰到了闭包。闭包是我感觉从学 Python 开始&#xff0c;用的时间最长的一小节课程 &#xff0c;44分钟的课程反反复复看了好几遍。 后来真的弄明白了&#xff…

作者头像 李华