☰
纯CSS与JavaScript实现智能呼吸灯:原理、参数与完整代码
2026/9/26 11:59:43 网站建设 项目流程

呼吸灯这个效果,在很多网页里都能见到——加载状态、消息通知、氛围装饰,一颗发光的圆点慢慢变亮又慢慢变暗,节奏和人的呼吸很像。用HTML实现“智能呼吸灯”,本质上就是用一个DOM元素、一段CSS动画、再加一点JavaScript交互,把这个自然节奏复刻到浏览器里。这篇文章不说虚的,直接从一个可运行的小项目出发,把实现原理、参数设计、常见坑一次讲透。刚学完HTML/CSS基础想练手的前端新人可以照着敲一遍,需要做提示灯、加载反馈、页面氛围效果的老手也可以把代码直接抄走。

1. 呼吸灯的“呼吸”到底是什么意思

1.1 效果拆解:一个周期里发生了什么

呼吸灯不是简单的“亮—灭”闪烁,而是一连串连续的亮度变化。一个完整周期通常包含三个阶段:从暗慢慢亮起,到达峰值后保持极短的一瞬间,再缓缓暗下来,回到起点。对应到网页里的视觉参数上,透明度(opacity)从0.2升到1,再平滑回落到0.2;元素尺寸(transform: scale)从0.9放大到1.1再缩回;如果还想做得更有氛围,可以用box-shadow模拟一圈光晕。

这三个维度缺一个都觉得“不像”。只做透明度,灯像在空气中淡入淡出,但少了“呼吸时身体微微起伏”的感觉;只做缩放,又像一个气球在吹气,没有光线变化。真正像呼吸的组合是:放大和变亮同时发生,缩小和变暗同时发生,光晕跟着扩散和收缩。这样看起来就是一颗有生命的灯珠,而不是一个会闪的圆点。

1.2 周期和节奏怎么定

成年人平静呼吸的频率大约是每分钟12到20次,也就是单次呼吸3到5秒。网页里的呼吸灯不需要严格对标人体,但节奏感非常重要。周期太长,超过6秒,用户会以为页面卡住了;周期太短,低于1秒,又变成了急促的闪烁,给人紧张感,和“呼吸”两个字就没什么关系了。

我实际做下来,比较舒服的区间是3秒到4秒。具体分法是:亮起过程占40%,暗下过程占40%,中间峰值停顿占20%左右。这个比例模拟的是正常呼吸——吸气之后有一个短暂的屏息,而不是刚到最亮就立刻回落。通过keyframes的百分比控制,你能精确还原这套节奏,这也是CSS动画比JS逐帧改透明度更优雅的原因之一。

1.3 技术选型:CSS动画还是JS定时器

很多人都踩过这个坑:一上来就用JavaScript写一个setInterval,每几十毫秒去改一次元素的透明度。这样能不能实现?能。但你要清楚,JavaScript跑在浏览器主线程上,页面只要有其他任务——滚动监听、布局计算、图片加载、接口返回后的DOM更新——透明度更新就可能会被插队,动画就变成一卡一卡的。

浏览器对CSS动画做了专门优化,transform、opacity这类属性走的是合成器线程,不占用主线程,所以能用CSS动画解决的就不要用JS去逐帧改。那需要交互怎么办?本次项目的思路是“CSS动画负责动,JS负责指挥”——用CSS变量把动画时长、颜色、亮度这些可调参数暴露出来,JS只修改变量,不直接改每一帧的样式。这样既保住了性能,又实现了“智能”控制。

2. 基础版:纯CSS让灯“亮起来再暗下去”

2.1 搭建HTML结构

呼吸灯需要一个载体。最简单的结构就是一个div,给它一个类名,然后全交给CSS处理。

<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>智能呼吸灯 - 基础版</title> <style> /* 样式稍后放入 */ </style> </head> <body> <div class="breath-light"></div> </body> </html>

这里有个容易被忽略的点:div默认是块级元素,宽高在没有内容的时候是0,直接写样式是看不见灯的。所以第一步是给这个div一个明确的width和height,再用border-radius: 50%把它变成圆形。至于摆放位置,可以让它居中,也可以放在页面的某个角落当指示灯,取决于你的使用场景。

2.2 用@keyframes定义呼吸动画

呼吸动画的核心在@keyframes。我们需要定义三个关键帧:起点(暗态)、中点(亮态)、终点(回到暗态)。

.breath-light { width: 120px; height: 120px; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(0,150,255,0.6)); /* 初始状态和动画第一帧保持一致,避免闪烁 */ opacity: 0.25; transform: scale(0.9); animation: breathe 3s ease-in-out infinite; } @keyframes breathe { 0% { opacity: 0.25; transform: scale(0.9); box-shadow: 0 0 10px 2px rgba(0,150,255,0.2); } 40% { opacity: 1; transform: scale(1.1); box-shadow: 0 0 40px 15px rgba(0,150,255,0.7); } 60% { opacity: 1; transform: scale(1.1); box-shadow: 0 0 40px 15px rgba(0,150,255,0.7); } 100% { opacity: 0.25; transform: scale(0.9); box-shadow: 0 0 10px 2px rgba(0,150,255,0.2); } }

animation这行是重点:breathe是动画名称,3s是单次周期时长,ease-in-out是缓动函数,infinite是无限循环。ease-in-out的意思是动画在起始和结束阶段速度慢、中段速度快,恰好对应呼吸“缓缓开始、缓缓结束”的自然状态。40%和60%处各停一下,模拟屏息瞬间。这样一条简单的动画,已经能看出呼吸感了。

2.3 让“光晕”更真实

呼吸灯之所以比普通圆点好看,核心在光晕。光晕用两层实现:第一层是background上用radial-gradient(径向渐变)做一个从中心亮到边缘暗的灯珠效果,第二层是box-shadow向外扩散的辉光。

radial-gradient的参数要理解:circle表示圆形渐变,第一个颜色是中心颜色,第二层是边缘颜色。如果你希望灯珠更接近真实LED,中心用接近白色的浅蓝,边缘用纯蓝色,模拟出“灯珠内部更亮”的视觉感受。

box-shadow这个属性值得多花点时间调。它有两个模糊半径参数:第一个是模糊距离,第二个是扩散距离。我做呼吸灯时的经验是:“0 0 40px 15px”这种组合在深色背景上效果最好,光晕均匀且不刺眼;如果是浅色页面,阴影的rgba透明度要降到0.3以下,否则会显得页面脏。

提示:别把box-shadow的模糊半径堆到100px以上。视觉上虽然更夸张,但浏览器绘制模糊阴影的计算量会明显增加,动起来容易掉帧。

3. 进阶版:让灯“聪明”起来

3.1 点击暂停与启动

一盏只会循环的灯其实不算“智能”。第一步要让它听从指挥:点击一下暂停,再点击继续。CSS里有一个专门干这事的属性——animation-play-state,取值paused时动画冻结,取值running时继续播放。

const light = document.querySelector('.breath-light'); const toggleBtn = document.getElementById('toggleBtn'); let isRunning = true; toggleBtn.addEventListener('click', () => { isRunning = !isRunning; light.style.animationPlayState = isRunning ? 'running' : 'paused'; toggleBtn.textContent = isRunning ? '暂停' : '启动'; });

需要说明的是,animation-play-state在切换时不会从头播放,而是保持在当前帧继续。也就是说,点击暂停后灯会停在那一刻的亮暗状态,再点击启动时,从停住的位置继续呼吸,这正好符合智能设备的直觉操作。

3.2 用CSS变量实现速度调节

想动态调整呼吸快慢,最直接的办法是用JavaScript修改animation-duration。但如果光只改这一项,动画会“跳”一下——因为浏览器需要重新计算动画周期。更平滑的做法是把动画时长定义为CSS变量,JS只需要改变量值。

.breath-light { --breath-duration: 3s; animation: breathe var(--breath-duration) ease-in-out infinite; }
const speedSlider = document.getElementById('speedSlider'); speedSlider.addEventListener('input', () => { light.style.setProperty('--breath-duration', speedSlider.value + 's'); });

CSS变量的好处是:修改它的值不会打断动画,浏览器会在下一个渲染帧自动调整动画时长。用户拖动滑块时,灯会从当前状态直接进入新的节奏,体验非常顺滑。这个模式是“CSS动画 + JS指挥”的核心,后续所有动态参数都可以照这个套路来做。

3.3 颜色切换与亮度控制

呼吸灯不能只有蓝色一种。我们可以把发光颜色也变成CSS变量,然后用一组色块让用户切换。这里有一点要特别注意:box-shadow不支持直接写“rgba(var(--color), 0.6)”,但可以在变量里存“0, 150, 255”这种RGB分量,然后配合rgba()函数使用。

.breath-light { --glow-rgb: 0, 150, 255; background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(var(--glow-rgb), 0.6)); } @keyframes breathe { 0% { opacity: 0.25; transform: scale(0.9); box-shadow: 0 0 10px 2px rgba(var(--glow-rgb), 0.2); } 40% { opacity: 1; transform: scale(1.1); box-shadow: 0 0 40px 15px rgba(var(--glow-rgb), 0.7); } 100% { opacity: 0.25; transform: scale(0.9); box-shadow: 0 0 10px 2px rgba(var(--glow-rgb), 0.2); } }

切换颜色时,JS只改一个变量:

function setColor(rgbString) { light.style.setProperty('--glow-rgb', rgbString); }

亮度控制则可以再引入一个变量,比如--max-opacity,然后在动画的最亮帧使用var(--max-opacity)。调暗灯光时把值从1改成0.7,灯就“收着亮”,不会破坏动画的节奏。

3.4 一键模拟“睡眠呼吸”模式

做这个项目时我发现一个好玩的需求:很多人想要一个“睡眠模式”,让灯的呼吸节奏变得更慢、更柔和,用来当夜灯或者专注时钟。实现方式是把时长变长、透明度上限压低、光晕缩小,同时切换背景色为暖黄色。

这其实就是把上面几个变量组合一下,做一个预设按钮:

function sleepMode() { light.style.setProperty('--breath-duration', '6s'); light.style.setProperty('--glow-rgb', '255, 180, 60'); light.style.setProperty('--max-opacity', '0.7'); }

“智能”就体现在这里:不是一条动画走到底,而是把参数组合成可切换的模式,用户能根据场景一键切换。这个思路放到真实项目里,就是一个完整的主题化交互组件了。

4. 完整可运行Demo:智能呼吸灯.html

4.1 完整代码

把上面的能力整合到一起,做一个带控制面板的完整页面。控制面板包含:暂停/启动按钮、速度滑块、四个颜色选项、睡眠模式按钮。全部代码只有一个HTML文件,复制到本地就能跑,没有任何依赖。

<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>智能呼吸灯</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #0f172a; font-family: Arial, sans-serif; -webkit-tap-highlight-color: transparent; } .stage { display: flex; align-items: center; justify-content: center; width: 100%; height: 320px; } .breath-light { width: 120px; height: 120px; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(var(--glow-rgb), 0.6)); box-shadow: 0 0 10px 2px rgba(var(--glow-rgb), 0.2); opacity: 0.25; transform: scale(0.9); animation: breathe var(--breath-duration) ease-in-out infinite; transition: box-shadow 0.3s ease; } @keyframes breathe { 0% { opacity: 0.25; transform: scale(0.9); box-shadow: 0 0 10px 2px rgba(var(--glow-rgb), 0.2); } 40% { opacity: var(--max-opacity, 1); transform: scale(1.1); box-shadow: 0 0 40px 15px rgba(var(--glow-rgb), 0.7); } 60% { opacity: var(--max-opacity, 1); transform: scale(1.1); box-shadow: 0 0 40px 15px rgba(var(--glow-rgb), 0.7); } 100% { opacity: 0.25; transform: scale(0.9); box-shadow: 0 0 10px 2px rgba(var(--glow-rgb), 0.2); } } .controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: center; padding: 16px 20px; background: rgba(255,255,255,0.06); border-radius: 16px; max-width: 440px; margin: 0 16px; } .controls button { border: none; padding: 10px 18px; border-radius: 10px; background: #3b82f6; color: #fff; font-size: 14px; cursor: pointer; } .controls button:hover { background: #2563eb; } .controls input[type="range"] { flex: 1; min-width: 140px; } .color-dots { display: flex; gap: 8px; } .color-dots span { width: 24px; height: 24px; border-radius: 50%; cursor: pointer; border: 2px solid rgba(255,255,255,0.3); } .color-dots span.active { border-color: #fff; } </style> </head> <body> <div class="stage"> <div class="breath-light" id="light"></div> </div> <div class="controls"> <button id="toggleBtn">暂停</button> <input type="range" id="speedSlider" min="2" max="6" step="0.5" value="3" title="呼吸速度"> <div class="color-dots"> <span>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询