
简介面向程序员的情人节、七夕或520表白网页源码包基于HTML5CSS3渲染页面结构并用JavaScript驱动倒计时、点击响应和动效播放让开发者在当天快速搭建有个人风格的互动表白页。压缩包共168个文件包含3个HTML页面、21个JS脚本、21个CSS样式表以及jpg/png图片、ttf/woff/eot字体和mp4演示视频等素材整体约124.51MB目录按前端资源类型分类便于替换文案、图片和配色。目前已有1445人学习下载适合前端初学者和需要快速出效果的开发者参考。资源覆盖面较完整既有响应式布局、多媒体嵌入、本地存储等技术实践也提供可直接部署的完整页面既能用于真实表白也能当作HTML5、CSS3和JavaScript的综合性练习帮助理解页面结构与动态交互的结合方式。1. 表白网页源码这事代码写得越简单越稳先泼一盆冷水你搜到的那些漫天飞舞的999 朵玫瑰雨开场表白网页源码大概率是几年前的静态页面套了个爱心粒子脚本真正值钱的部分不是动画多炫而是它能在任何设备上打开、不会被浏览器拦截、且对方愿意看完。程序员做这种网页最大的优势是知道边界——哪些效果用 CSS 就能撑住哪些必须上 Canvas哪些交互在手机浏览器上根本触发不了。这篇文章就按一条最可靠的路线来讲从最小骨架开始逐步把粒子、倒计时、移动端适配、局域网交付一次讲完。读者不管是给自己表白用还是接个外快帮别人做定制页照着这份思路改参数就能出活。2. 表白网页源码的最小骨架倒计时、按钮与状态切换2.1 先用一个 HTML 文件把页面立起来这类表白网页源码的共同点是单文件。一个index.html把 CSS 和 JS 都塞进去好处是交付成本极低——你可以直接用微信把文件发给对方或者丢到任意静态托管服务上。不要一上来就引 Vue、React 或者搞npm run dev对方打开的是网页不是你的开发环境。先看基础结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1, maximum-scale1 title给某个人的一封网页情书/title style body { margin: 0; min-height: 100vh; background: linear-gradient(135deg, #fce4ec, #f8bbd0, #f48fb1); font-family: -apple-system, PingFang SC, Microsoft YaHei, sans-serif; } .wrap { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 20px; box-sizing: border-box; } #timer { font-size: 28px; font-weight: 600; color: #ad1457; margin-bottom: 12px; } .avatar-box img { width: 140px; height: 140px; border-radius: 50%; object-fit: cover; box-shadow: 0 8px 24px rgba(173, 20, 87, .25); } .btn { min-width: 96px; padding: 12px 24px; margin: 16px 8px 0; border: 0; border-radius: 40px; font-size: 18px; background: #ad1457; color: #fff; } /style /head body div classwrap div classavatar-boximg srcphoto.jpg alt照片/div div idtimer/div div idquestion有一件事想问你/div button classbtn idbtnYes同意/button button classbtn idbtnNo我再想想/button /div script // 核心逻辑写在下面 /script /body /html这段代码背后的选型逻辑是手机和电脑都读得懂-apple-system与PingFang SC这套字体栈即使没有安装任何第三方字体也不会乱min-height: 100vh保证全屏铺满而不是用height写死——因为浏览器地址栏伸缩会导致100vh变化这个坑后面第 4 章专门讲。meta: viewport如果漏了移动端会按 980px 渲染页面字就小了这一步不是可选项。2.2 倒计时逻辑与时间解析的时区陷阱倒计时是这类网页的标配。写之前要决定一件事倒计时目标是某个固定时刻还是从打开页面起第 N 天。前者用绝对时间后者用相对时间。绝大多数场景用绝对时间因为你希望对方在某天零点看到特定效果。// 目标时间注意没有写时区后缀 // 常见做法是在本地测试时拿 new Date() 打印确认 const target new Date(2025-02-14T00:00:00).getTime(); function tick() { const now Date.now(); const diff Math.max(0, target - now); const d Math.floor(diff / 86400000); const h Math.floor((diff % 86400000) / 3600000); const m Math.floor((diff % 3600000) / 60000); const s Math.floor((diff % 60000) / 1000); const el document.getElementById(timer); if (diff 0) { el.textContent 这一刻来了; clearInterval(interval); return; } el.textContent d 天 h 时 m 分 s 秒; } tick(); const interval setInterval(tick, 1000);这里有一个程序员容易忽略的细节new Date(2025-02-14T00:00:00)不带时区后缀时按浏览器本地时区解析。你本机测试没问题但如果部署的服务器在别的时区或者对方手机设置了海外时区倒计时会直接差出几个小时。为了避免这个问题我更推荐先把目标时间戳算好const target new Date(2025-02-14T00:00:0008:00).getTime()。如果你就是要在对方本地时间的零点生效那保持无后缀写法反而正确。想清楚你的语义比复制代码更关键。另一个边界是diff为负数时不能出现-1 天 -2 分上面用Math.max(0, ...)把负值截断再配合clearInterval收尾。注意setInterval在标签页切到后台时会被浏览器节流秒数会卡但对倒计时场景影响不大真正要追求秒级同步就改用Date.now()差值而不是累加秒数这里已经是这个方案。2.3 按钮交互不要让拒绝按钮真的拒绝你按钮逻辑是这类网页的灵魂。最土的实现反而是最稳的同意按钮点击后页面切换到表白成功状态我再想想按钮点击后文案变化且按钮本身越来越大或者位置轻微偏移。不要做成两个按钮的简单二选一那叫电子问卷。const texts [ 真的不再想想吗, 要不再看看我, 你跑不掉的, 点了同意就可以领走一个我 ]; let idx 0; const btnNo document.getElementById(btnNo); const btnYes document.getElementById(btnYes); const question document.getElementById(question); btnNo.addEventListener(click, function() { if (idx texts.length) { question.textContent texts[idx]; idx; } // 按钮变大注意每次点击都会触发一次要防抖 const scale 1 idx * 0.15; btnYes.style.transform scale( Math.min(scale, 1.8) ); }); btnYes.addEventListener(click, function() { question.textContent 我就知道你会同意; // 这里可以触发满屏粒子或彩带下一章细说 btnNo.style.display none; btnYes.style.transform none; });这段代码里最值得说的是防抖问题。btnYes.style.transform每次都从初始状态重新计算不会累积所以效果是逐渐变大这是合理的。但如果你在这个回调里创建多个定时器给按钮加动画必须先在函数顶部clearInterval清掉旧定时器否则快速连点会叠加出残影或者动画突然加速。另一个细节scale超过 1.8 后按钮可能超出安全区域所以要封顶。按钮被顶出视口后用户点不到某些安卓浏览器上position: fixed还会被地址栏遮挡这些都是验收时要过一遍的真实场景。3. 让表白网页源码活起来粒子动画、背景音乐与参数调优3.1 背景效果选 CSS 动画还是 Canvas搜到的很多表白源码里背景飘的是爱心实现方式分两派纯 CSS 关键帧和Canvas 粒子系统。我的选择标准很简单——粒子数量少于 40 个用 CSS多于 40 个用 Canvas。CSS 动画由浏览器合成器处理不占 JS 主线程省电且代码量极少Canvas 适合做点击后爆出几十颗爱心这种瞬时粒子因为 DOM 节点瞬间创建和销毁的代价远高于 Canvas 绘制。先看 CSS 实现一个飘落爱心的成本keyframes fall { 0% { transform: translateY(-10vh) rotate(0deg); opacity: 0; } 10% { opacity: 1; } 100% { transform: translateY(110vh) rotate(360deg); opacity: .6; } } .heart { position: fixed; top: -10vh; left: 20%; font-size: 22px; animation: fall 6s linear infinite; }如果你准备在页面里放 20 个这样的爱心就先每个给不同的left和delay。但别用内联样式逐个手写用 JS 批量生成const hearts [❤️, , , ]; // 这里改成你想用的字符 for (let i 0; i 24; i) { const el document.createElement(div); el.className heart; el.textContent hearts[i % hearts.length]; el.style.left Math.random() * 100 vw; el.style.animationDelay (Math.random() * 8) s; el.style.animationDuration (6 Math.random() * 4) s; document.body.appendChild(el); }注意left用了vw而不是%因为元素position: fixed时%相对的是视口宽度vw也是视口宽写哪个都行但混用容易混乱。这里给每个粒子随机 6 到 10 秒的时长是为了避免所有爱心同时落地后出现空窗期。真正在做这种网页源码时我会先把粒子数、时长、延迟三个参数抽成对象方便一次性调const conf { total: 24, baseDuration: 6, randomRange: 4, maxLeft: 100, emoji: [❤️, ] };参数含义很直白total是同时存在的粒子数baseDuration决定下落速度值越小飘得越快randomRange负责打散节奏。这三个参数是这类页面里最核心的调优点给对方看之前一定先试跑两分钟确认没有爱心在同一条垂直线上下落或者底部堆成一坨的情况——前者是因为随机种子没拉开后者是粒子没有在页面底部及时消失多数动画源码里通过top: 110vh配合opacity渐隐解决只要别写成top: 100%那会在视口底部突然消失而不是飘出屏幕。3.2 Canvas 点击反馈一次性粒子的正确写法同意按钮按下去最好有一团粒子爆开。用 DOM 做 30 个爆炸爱心会把界面卡出明显顿挫Canvas 是更合理的实现路径。const canvas document.getElementById(fx); const ctx canvas.getContext(2d); canvas.width window.innerWidth; canvas.height window.innerHeight; const particles []; function burst(x, y) { for (let i 0; i 24; i) { const angle Math.random() * Math.PI * 2; const speed 3 Math.random() * 5; particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 3, life: 1, // 1 到 0递减 size: 6 Math.random() * 10 }); } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i particles.length - 1; i 0; i--) { const p particles[i]; p.x p.vx; p.y p.vy; p.vy 0.12; // 模拟重力 p.life - 0.015; if (p.life 0) { particles.splice(i, 1); continue; } ctx.globalAlpha p.life; ctx.fillStyle #e91e63; ctx.beginPath(); ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2); ctx.fill(); } requestAnimationFrame(draw); } draw(); btnYes.addEventListener(click, function(e) { burst(e.clientX, e.clientY); });这段代码里有两个必须保留的细节。第一是ctx.clearRect放在绘制函数开头漏了就会变成拖影特效虽然某些场景刻意留残影有装饰感但表白页里满屏拖影反而显得脏。第二是粒子数组用了正向push、倒序splice的写法是因为splice会改变数组索引从尾部往前删可以避免跳过元素。life同时控制透明度和半径p.size * p.life让粒子有炸开后变淡缩小的自然衰减这比固定半径更接近真实火花效果。vy 0.12是重力常数你可以把它想象成爆炸上抛、回落后被地心引力拽下来。想热烈一点就把 0.12 调成 0.08想下落干脆就调成 0.3。3.3 背景音乐的自动播放策略又是一个被骂烂但每次都要处理的规则移动端 Safari 会阻止页面的自动播放只有用户发生触摸交互后audio.play()才算合法。不要试图通过设置autoplay属性绕过直接面对它。audio idbgm loop preloadauto source srcsong.mp3 typeaudio/mpeg /audio// 首次点击任意按钮时启动音乐 function startMusic() { const audio document.getElementById(bgm); if (audio.paused) audio.play().catch(function(){}); } document.body.addEventListener(touchstart, startMusic, { once: true }); btnYes.addEventListener(click, startMusic);说下这段的细节{ once: true }是给touchstart用的保证只挂一次监听audio.play()返回的是一个 Promise在桌面浏览器里没问题在部分安卓 WebView 里会因为没有用户手势而 reject所以必须挂.catch防止控制台报 Unhandled Rejection。歌曲文件别放太大建议压到 3MB 以内96kbps 的 mp3 一首歌大约 2MB。图片也一样photo.jpg压缩到 200KB 以内整个页面的目标就是4G 网络下 3 秒内能完整加载。为什么强调这个因为表白网页源码通常是靠微信、QQ 或一个临时链接发出去的不是每个人都站在 Wi-Fi 下点开网络差时一张原图就能让页面白屏十秒。4. 表白网页在手机上的适配viewport、动态视口与触摸延迟4.1 100vh 在移动端的三个表现和兜底方案写桌面端时min-height: 100vh不会出问题但拿到手机上看就会碰到三种情况iOS Safari 的地址栏收起前100vh是屏幕的视觉高度地址栏收起后100vh变成了加长的高度部分安卓浏览器的100vh直接是满屏高度导致页面底部被导航条遮挡。这个现象的核心原因是移动端浏览器的视口是动态的vh单位永远等于最大化视口而不是当前可视区。先给一个当前最合适的 CSS 写法.wrap { min-height: 100vh; /* 旧浏览器兜底 */ min-height: 100dvh; /* 动态视口Safari 16.4 与 Chrome 108 */ min-height: 100svh; /* 小视口防止地址栏展开时底部按钮被压出屏幕 */ }dvh是动态视口地址栏收起时它跟着变svh是最小视口尺寸在地址栏占位时取的是偏保守的值。如果你的目标用户里有相当一部分在用两年前的安卓机还需要 JS 兜底一段用window.innerHeight给.wrap赋值高度并监听resize事件重算。注意要在orientationchange时也要重算这个事件在安卓手机上经常被漏掉。function setRealHeight() { const h window.innerHeight; document.querySelector(.wrap).style.minHeight h px; } window.addEventListener(resize, setRealHeight); window.addEventListener(orientationchange, function() { setTimeout(setRealHeight, 200); }); setRealHeight();setTimeout(200)是为了等 iOS Safari 完成转场动画后再取高度转场过程中innerHeight的值是旧值直接赋值会闪一下。这种细节不写出来真机测试时通常要查半天。4.2 去掉 300ms 点击延迟与误触移动端双击缩放是 300ms 延迟的根源。现在大多数浏览器已经对设置了viewport的页面不再等双击判定但低成本加一行更保险button { touch-action: manipulation; }touch-action: manipulation的意思是只允许滚动和缩放以外的触摸行为浏览器可以放心在手指按下时立即触发点击不用等双击判定。这个属性同时还有另一个作用禁掉长按按钮时呼出的系统菜单对这类全屏点击的页面能明显提高安全感。第二个针对误触的点两个按钮间距不要小于 8px否则手小的人容易点错。很多表白源码喜欢把两个按钮贴在一起制造喜剧效果但真实使用场景里对方是认真点的误触会直接破坏氛围。我会把按钮间距设在 16px 以上且保证热区不小于 44x44pt这是苹果人机交互指南里的最小值。4.3 减弱动态效果prefers-reduced-motion 降级不是所有用户的手机都能跑满 60fps 的粒子动画。低价安卓机 微信内置浏览器24 个 CSS 爱心可能直接拖到 20 帧。一个更负责任的做法是主动检测用户系统级的减弱动态效果偏好media (prefers-reduced-motion: reduce) { .heart { display: none; } }const reduceMotion window.matchMedia((prefers-reduced-motion: reduce)).matches; if (reduceMotion) { // 不挂 Canvas 动画只保留静态渐变背景 canvas.style.display none; }这里要说明prefers-reduced-motion不是给你做访问者分级用的它的语义是用户明确表示不喜欢动态效果。对这种表白页来说对方关闭动态效果的概率其实很低但你把它处理好了是一个很亮眼的加分项——对方手机卡顿时会自动降级而不是一直在那转圈。真正决定性能的指标是粒子数我通常把上限控制在 36 个以内并让粒子数根据屏幕宽度缩放比例const total Math.min(36, Math.floor(window.innerWidth / 36));这段话的意思很简单屏幕越宽能容纳的粒子越多但 36 个是硬顶。超过这个数就算桌面端能跑放到手机上也会发热——特别是微信内置浏览器对 Canvas 的硬件加速支持不稳定粒子一旦铺满屏幕电量会肉眼可见地往下掉。5. 把表白网页源码交付出去本地预览、局域网访问与彩蛋位5.1 不要用 file:// 直接打开先起一个本地服务交付这个网页时最常见的错误是直接把index.html文件丢给对方。用file://协议打开会有三个隐患Canvas 在某些浏览器里读取本地图片会触发跨域限制、音频无法正确加载、微信内置浏览器直接打开本地文件的行为不可控。所以交付前至少先在本地起一个静态服务验证一次。cd 表白页目录 python -m http.server 8080然后在浏览器打开http://localhost:8080预览。python -m http.server是 Python 3 自带的静态文件服务零依赖。如果对方和你不在同一台电脑需要把页面放到你的电脑上后开启同一个 Wi-Fi把http://你的局域网IP:8080发过去。查看局域网 IP 常用命令是 Windows 下ipconfigmacOS 下ifconfig | grep inet。注意 Windows 防火墙首次会弹拦截框选允许访问另外手机和电脑必须连同一个路由器的同一个网段部分酒店或访客 Wi-Fi 默认开启了 AP 隔离这时候两台设备互访会失败调试半天都不是代码问题。如果不想依赖局域网环境直接把这个文件夹上传到任何支持静态托管的服务上即可。这类页面不需要后端一个 HTML 加两张图片就是一个完全体。上传后记得验证一下 https 访问会不会被浏览器拦截证书过期这类事尤其不能发生在表白日当天。5.2 三个面向程序员的彩蛋位既然接收方是程序员这类源码在网上流通的第 N 手版本早就不新鲜了真正的亮点往往在细节里。我一般会在交付前留三个彩蛋位第一个是浏览器控制台输出一段话第二个是页面标题栏的交替变化第三个是隐藏的键盘交互。console.log(%c 你好我是那个写 HTML 的人 , background:#ad1457;color:#fff;font-size:16px;padding:8px;); // 标题栏每隔 5 秒切换一次利用 document.title const titles [在吗, 有个问题想问你, 看下页面右下角]; let ti 0; setInterval(function() { document.title titles[ti % titles.length]; ti; }, 5000);这里特别提一下console.log的%c占位符第一个参数里的%c后面跟着样式字符串就能在控制台输出带背景色和补白的文字。这个彩蛋的作用不是炫技而是让懂行的人发现这个页面是特意做给我看的对方 F12 一开就有回应。还有一个容易忽略的细节交付前把页面里所有写死的名字、日期、照片路径检查一遍。网上的源码模板大多自带某人的名字或某个特定的日期你要改的是title、question的初始文案、和目标日期target这一行。尤其注意photo.jpg的路径如果图片放在子目录里img src要对应改成images/photo.jpg。最后用无痕窗口打开一次页面模拟对方第一次点击的完整路径从浏览器加载、看到倒计时、点按钮、听到背景音乐、直到成功页面出现一套流程走通再发出去。本文还有配套的精品资源点击获取