简介:面向程序员与前端学习者的创意表白网页源码包,专为情人节、七夕、520等浪漫节点设计,核心基于HTML、CSS与JavaScript,可快速生成动态互动表白页面。包内整合情侣空间单页与520表白页两套完整场景,包含爱心飘落、打字机告白、倒计时、背景音乐、相册轮播等经典交互模块,支持二次修改与个性化定制,页面自带多套视觉风格,动态背景可随鼠标变化,配合轻量脚本即可呈现惊喜效果。资源共51个文件,压缩包约14MB,以jpg、gif、png图片素材为主,辅以js交互脚本、css样式、html入口页,并含mp4背景视频与ico图标,结构完整,打开即可预览效果,便于对照学习前端动效实现。目前已有2026人学习下载,适合想用代码表达心意或练习前端动效的开发者。通过实际改造页面,既能快速产出高颜值表白作品,也能深入理解DOM操作、动画事件与响应式布局等知识点。
1. 从表白页到前端练手,这个源码的打开方式不止一种
情人节、七夕、520,每年这三个节点前后,“表白网页源码”的搜索量都会准时涨一波。多数人搜到的是现成压缩包,下载、解压、双击 index.html,能看就完事。但作为程序员,我更建议把这个标题拆成两个问题来看:一是“怎么用最少成本做出一个能拿得出手的动态页面”,二是“这个页面除了表白,还能帮你练什么”。
一个不依赖后端、不依赖框架的 HTML 单页,严格来说就是三样东西:语义化的结构、能撑起氛围的视觉层、以及几段不复杂的 JavaScript。动画不必炫技,心形粒子、漂浮光点、打字机文案,这三样组合起来就足够覆盖大多数场景。本文会从原生三件套讲到一个可部署的完整方案,包含参数调整、移动端适配和踩坑点,新手能照着做,有经验的人也能在动画调度和部署细节里找到自己关心的东西。
2. 表白网页的骨架:HTML 结构与 CSS 视觉层怎么搭
2.1 页面结构:不要一上来就写满整个 DOM
常见的表白页源码,结构上往往只有几个核心区块:全屏背景、居中卡片、按钮区域、文案区域。很多人第一次写的时候喜欢把每个字都塞进一个 div,结果后期改样式时类名混乱、层级嵌套过深。我一般会先把语义层拆干净:section承载主体内容,canvas留给特效层,按钮和文案单独拎出来作为交互层。
<main class="love-page"> <canvas id="bg-canvas"></canvas> <section class="hero"> <div class="hero-inner"> <h1 id="typewriter"></h1> <p class="subtitle">想把这段话,说给你听</p> <div class="actions"> <button id="btn-yes">我愿意</button> <button id="btn-no" class="secondary">我再想想</button> </div> </div> </section> </main>这段结构里,canvas固定挂在底层,section.hero用绝对定位盖在上面,按钮区域独立成块,方便后面挂事件。注意h1里没有写死文案,留给后面打字机效果逐字写入。subtitle是静态副文案,保证 JavaScript 加载失败时页面也不至于空白——这是很多源码容易忽略的降级处理。
2.2 视觉层:渐变背景、卡片动效与字体节奏
表白页的视觉核心不在复杂,而在氛围。深色渐变底配浅色文字,是性价比最高的组合,既能让发光类特效显眼,也避免大面积纯白造成的廉价感。
.love-page { position: relative; width: 100%; height: 100vh; overflow: hidden; background: linear-gradient(135deg, #1a0b2e 0%, #2d1b4e 50%, #4a1942 100%); font-family: "PingFang SC", "Microsoft YaHei", sans-serif; } .hero { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; z-index: 10; } .hero-inner { background: rgba(255, 255, 255, 0.06); backdrop-filter: blur(8px); padding: 48px 56px; border-radius: 24px; border: 1px solid rgba(255, 255, 255, 0.15); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); transform: translateY(0); animation: float-in 1.2s ease-out; } @keyframes float-in { 0% { opacity: 0; transform: translateY(30px); } 100% { opacity: 1; transform: translateY(0); } }渐变方向选了135deg,从左上到右下,比垂直渐变多了些层次。backdrop-filter: blur(8px)是毛玻璃效果的关键,但要注意它在部分低端安卓浏览器上会失效,所以后面还叠了一层半透明背景色作为兜底。float-in动画让卡片在页面加载时有一个轻微上浮的入场过程,时长 1.2 秒,过快会显得仓促,过慢则让人等着急。
这里有一个容易被忽视的细节:height: 100vh在移动端地址栏收起时会出现跳动,推荐改用100dvh,即动态视口高度。旧浏览器不认识这个单位,就写两行,一行兜底一行渐进增强。
3. 交互层的 JavaScript:从打字机到按钮反馈
3.1 打字机效果:一种写法,多处复用
打字机是表白页的高频元素,实现思路不复杂:把一段完整文案拆成字符数组,用固定间隔逐个推入元素。常见源码里用setInterval写,我的习惯是用setTimeout链式递归,好处是每轮间隔可以单独计算,比如遇到标点符号时停顿更久,模拟真实说话节奏。
function typeWriter(el, text, speed = 80, done) { let i = 0; (function next() { if (i <= text.length) { el.textContent = text.slice(0, i++); const pause = text[i - 1] && /[,。!?,.!?]/.test(text[i - 1]) ? speed * 4 : speed; setTimeout(next, pause); } else { done && done(); } })(); } typeWriter(document.getElementById("typewriter"), "遇见你之后,每个节日都有了名字。", 70);核心在递归函数next内部:每次取text.slice(0, i++)写入元素,这样不会产生对innerHTML的重复解析。标点检测通过正则[,。!?,.!?]实现,命中时停顿时间放大四倍,视觉上更接近人声。done回调可以串联下一段动画,比如打字完成后开始播放背景音乐或触发心形爆发。
实际跑这个函数时你会发现一个潜在问题:如果用户在打字过程中点击了按钮,递归仍然在进行。所以更稳的写法是保留一个定时器句柄,在外部必要时调用clearTimeout。源码压缩包里很少处理这个,但自己写的时候值得留意,尤其是文案较长时。
3.2 “我愿意”与“我再想想”:按钮逻辑的两种设计
这个交互是表白页的灵魂,也是最容易写砸的部分。常见的做法是:点击“我再想想”,按钮本身开始“逃跑”——每次 hover 或点击就换一个随机位置;点击“我愿意”,页面切换为庆祝状态。
const btnNo = document.getElementById("btn-no"); const btnYes = document.getElementById("btn-yes"); btnNo.addEventListener("click", () => { const { innerWidth: w, innerHeight: h } = window; const btnW = btnNo.offsetWidth, btnH = btnNo.offsetHeight; const maxX = w - btnW - 20; const maxY = h - btnH - 20; btnNo.style.position = "fixed"; btnNo.style.left = Math.random() * maxX + "px"; btnNo.style.top = Math.random() * maxY + "px"; }); btnYes.addEventListener("click", () => { document.querySelector(".hero-inner").innerHTML = ` <h2 style="font-size: 2.2rem;">好呀</h2> <p>今天是 ${new Date().toLocaleDateString("zh-CN")},值得记住。</p> `; launchHearts(); });“逃跑”逻辑用position: fixed让按钮脱离文档流,再通过Math.random()计算新坐标,边界减掉按钮自身宽高和 20px 间距,防止按钮跳出视口。点击事件触发后再按一次,按钮已经没有原来的监听上下文了,这里用了最简单的“全局替换”思路。launchHearts()是在这个场景下触发的心形粒子爆发函数,下一节展开。
需要特别说明的是“逃跑”按钮在触屏设备上的体验:移动端没有 hover,只有点击,所以随机跳转的时间点只能在click里写,不能依赖mouseenter。
3.3 canvas 粒子系统:心形形成与光点漂浮
到了这一步,页面已经有了结构和交互,但还缺少“氛围层”。最常用的方案是 canvas 粒子:一部分粒子组成一个大心形,缓慢旋转并漂移;另一部分作为背景光点自由浮动。心形公式并不复杂,用参数方程把粒子坐标算好,再通过 requestAnimationFrame 逐帧绘制。
const canvas = document.getElementById("bg-canvas"); const ctx = canvas.getContext("2d"); let particles = []; let mouseX = innerWidth / 2; function resize() { canvas.width = innerWidth; canvas.height = innerHeight; } window.addEventListener("resize", resize); resize(); const HEART_POINTS = 800; for (let i = 0; i < HEART_POINTS; i++) { const t = (i / HEART_POINTS) * Math.PI * 2; const x = 16 * Math.pow(Math.sin(t), 3); const y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)); particles.push({ baseX: x * 5, baseY: y * 5, x: x * 5, y: y * 5, offsetX: (Math.random() - 0.5) * 4, offsetY: (Math.random() - 0.5) * 4, size: Math.random() * 1.6 + 0.6, alpha: Math.random() * 0.5 + 0.3 }); } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); const cx = canvas.width / 2; const cy = canvas.height / 2; particles.forEach(p => { p.x += (p.baseX - p.x + p.offsetX) * 0.05; p.y += (p.baseY - p.y + p.offsetY) * 0.05; ctx.beginPath(); ctx.arc(cx + p.x, cy + p.y, p.size, 0, Math.PI * 2); ctx.fillStyle = `rgba(255, 105, 150, ${p.alpha})`; ctx.fill(); }); requestAnimationFrame(animate); } animate();这里的心形是经典心形参数方程,x = 16sin³(t),y方向是三项余弦的组合。系数5控制缩放尺寸,粒子初始坐标故意与目标坐标有偏差,然后在每帧中按0.05的系数向目标逼近,这就产生粒子“聚拢成心”的入场效果——比直接画一个静止的心形有视觉张力。
offsetX/offsetY的范围是正负 2,用于让粒子最终不在精确位置停下,而是围绕目标点轻微抖动,模拟呼吸感。整个动画循环用requestAnimationFrame驱动,它在页面切换标签页时会自动降帧,节省 CPU,比写固定setInterval高效得多。
4. 把 HTML、CSS、JS 合成可交付源码:目录组织与部署
4.1 文件组织:单文件与三文件的选择
网上流传的很多表白源码是一个 index.html 里塞满<style>和<script>,好处是双击就能用,拷贝方便。但从开发体验出发,我建议三个核心文件分开:
love-page/ ├── index.html ├── css/ │ └── style.css ├── js/ │ ├── typewriter.js │ ├── particles.js │ └── main.js这样做的直接好处是:typewriter.js和particles.js都是无 DOM 依赖的纯逻辑模块,下次做别的页面时可以直接复用。到了交付阶段,再通过简单工具把 CSS 和 JS 内联进 HTML 生成单文件版——这正好对应两种使用场景:开发时多文件协作,发布时单文件分发。
如果你需要快速合并,不需要引入 webpack 这类重工具,用npx html-inline一条命令就能把外部资源嵌入到 html 中。
4.2 本地预览与局域网分享
完成编码后的第一件事是确认页面在本地运行正常。直接双击 index.html 打开file://协议访问时,会有一个典型问题:如果页面里引入了外部字体或图片资源,跨域请求会被浏览器拦截。解决方案是起一个本地静态服务器。
# 在 love-page 目录下执行 python3 -m http.server 8080然后浏览器访问http://localhost:8080。http.server是 Python 自带模块,不需要安装任何依赖。端口可以改为其他值,比如 3000 或 5173,只要不冲突。
服务器跑起来后,同一局域网内的手机想预览,就查到本机局域网 IP。macOS 用ipconfig getifaddr en0,Linux 用hostname -I,Windows 用ipconfig。手机浏览器访问http://你的IP:8080就能看到效果。这一步对于真实表白场景非常重要——你大概率需要让对方在手机上看页面,而不是在电脑上。
4.3 部署到公网:让链接可分享
如果不想局限于局域网,下一步是部署到公网。最省事的选择是 GitHub Pages 或 Gitee Pages,免费、无需服务器、支持 HTTPS。以 GitHub Pages 为例,在仓库设置里把分支指定为main,根目录/,保存后通常几十秒内就能访问。
如果你的场景需要“发一个链接过去,对方点开就是完整页面”,部署是绕不开的。需要注意,若页面里引用了外部图片,仓库里要一并放进去,且路径最好用相对路径——否则部署到二级域名路径下时会 404。
4.4 服务器部署参数:Nginx 配置要点
自己手里有服务器的人,更推荐用 Nginx 托管,可控性更强。配置很简单,但有两个参数值得关注。
server { listen 80; server_name your-domain.com; root /var/www/love-page; index index.html; location / { try_files $uri $uri/ /index.html; } gzip on; gzip_types text/css application/javascript text/html; }try_files $uri $uri/ /index.html在单页应用里是为了前端路由,表白页没有路由需求,但保留它无妨,深层刷新不会 404。gzip on加上后面的类型声明,能把 CSS 和 JS 压缩掉 60% 以上,页面体积变小时加载明显更快。证书方面现在 Let‘s Encrypt 免费,配置一次就自动续期。
5. 让表白页跑得更稳的进阶配置与常见踩坑
5.1 音乐自动播放的三个坑
很多表白源码都尝试在页面加载时自动播放背景音乐。现实是,桌面端 Chrome、Safari 的自动播放策略要求必须有用户手势,否则音频被静默拦截。常见的绕过方法是进入页面后弹出“开启声音”按钮,用户点击后才能播放。
const audio = new Audio("bgm.mp3"); audio.loop = true; audio.volume = 0.4; const startBtn = document.getElementById("btn-start"); startBtn.addEventListener("click", () => { audio.play().then(() => { startBtn.style.display = "none"; }).catch(e => console.warn("play rejected:", e)); });这里没有伪装成自动播放,而是把按钮当成娱乐场入口:点击“开始”后播放音乐并隐藏按钮。这种交互在移动端 100% 兼容,也不用担心浏览器策略更新导致失效。audio.volume = 0.4是为了避免一上来音量过大吓到人,具体数值可按场景调整。
移动端还有一个坑:音频文件体积过大时,在弱网下会一直处于加载状态,导致点击按钮没有反应。解决方案是把音频压缩到 1–2MB 以内,或改用 CDN 托管音频文件。
5.2 移动端适配:rem 与 vw 的取舍
表白页大概率是在手机上看的,所以适配不能靠“缩一缩就行”。一个稳妥的做法是把 body 的字号基准与视口宽度绑定,让所有 em/rem 单位随屏幕等比放大。
html { font-size: 18px; } @media screen and (min-width: 375px) { html { font-size: 20px; } } @media screen and (min-width: 414px) { html { font-size: 22px; } }之前提到过100vh的移动端跳动问题,推荐在.love-page上同时写height: 100vh; height: 100dvh;,支持动态视口的浏览器用后者,不支持就用前者。如果你用了 canvas 全屏特效,别忘了在resize事件里同步更新 canvas 的宽高,否则旋转屏幕后画面会拉伸变形。
触屏设备的另一个细节是双击缩放会干扰按钮响应,只要没有用户主动缩放的需求,可以在 head 里加上<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />。注意不要拼上user-scalable=no,那对无障碍不友好。
5.3 性能优化:粒子数量与动画帧率调优
粒子数量直接决定帧率。8 行的老电脑上跑 1500 个粒子,每帧绘制 1500 次arc会吃力,肉眼可见卡顿。我的建议是:心形主体粒子 600 到 800 个,背景光点不超过 200 个,总计控制在 1000 以内,视觉密度足够。
如果真的需要更多粒子,可以做一个小优化:接近目标位置后粒子每 3 帧才重绘一次,因为此时视觉变化极小,没必要每帧都在画。具体实现可以维护一个帧计数器,frame % 3 === 0时才绘制粒子帧,其他帧直接clearRect即可。
还有一个容易被忽略的问题:canvas 的fillStyle每次循环都被重新赋值,浏览器需要在内部做字符串解析。把颜色预先计算成rgba()字符串缓存到粒子对象上,重绘时的解析开销会降不少。
5.4 用 URL 参数做多场景切换
一个表
本文还有配套的精品资源,点击获取