
简介HTML5跑马灯抽奖页面是一套面向前端初学者与活动运营人员的可直接运行的互动抽奖源码适用于电商促销、年会庆典、线下大屏互动等场景。资源包内共19个文件其中包含1个HTML结构页、1个JavaScript交互逻辑文件、1个CSS样式表以及15张PNG图片和1张JPG背景图较完整地覆盖了抽奖页面所需的页面骨架、动画控制、响应式布局和视觉素材。整个压缩包仅167KB轻量易部署适合快速修改后嵌入现有活动页。已有758人学习下载可供参考的实践价值较高。通过这份资源不仅能直接获得可用的跑马灯抽奖页面还能从中理解jQuery控制滚动抽奖、CSS3动画与过渡实现视觉反馈、移动端触摸事件适配等常见前端技巧尤其适合希望动手复现或二次开发同类活动的开发者。 年会、班级活动、单身节派对大屏上滚动的名单慢慢减速最后定格在一条名字上全场欢呼——这就是html5跑马灯抽奖页面最典型的画面。很多人第一次接触它是在「网页设计作业」里老师要求做一个有点交互感的页面跑马灯抽奖因为视觉热闹、逻辑边界清晰成了热门选题。可如果只是追求「能滚动」几行代码就够了但要做到能拿上活动大屏、现场不出岔子光靠滚动效果还不够得把玩法定位和技术方案一起想明白。跑马灯抽奖和九宫格、转盘最大的区别在于「过程可见」。九宫格、转盘的核心是「转一下直接出结果」跑马灯却是让所有名字在大屏上轮流经过观众全程盯着名单在动悬念被拉得很长。最后停在哪个名字反而不是最关键最关键的是「减速那两秒」——所有人都在看着一行名字由快到慢屏住呼吸。这种节奏决定了页面交互设计速度要有明显变化停止点要有明确反馈不能拖沓也不能戛然而止。下面我拆开讲整个实现思路从标签选型到动画收尾代码都能直接用。1. 跑马灯抽奖要解决的三个核心问题滚动、停止、反馈跑马灯抽奖页面看着简单但要让它真正成立其实藏着三个必须解决的问题名单怎么滚、滚动怎么停下来、停下来之后怎么让全场知道结果。这三个问题分别对应布局方案、动画方案和反馈方案。很多人一上来就急着写滚动效果结果往往是在「名单能跑」这一步停了太久真正影响现场体验的停止控制和结果反馈却没做页面交上去只能说「能动」谈不上「能用」。先看「怎么滚」。常见的跑马灯玩法可以分成横向广播式和纵向卡片式两种。横向的是一行名字或祝福语从屏幕一侧跑到另一侧适合抽「一群幸运观众」奖品区显示抽中了多少个整体感觉更像翻滚字幕纵向的是名单像电梯一样向下滚动适合抽「唯一的幸运儿」大屏上可以同时展示这个人的名字、头像和信息卡片。两种形态我建议都做横向那条放在页面顶部当装饰条纵向那条做主抽奖区。实际项目里横向装饰用CSS动画就够了因为它不需要精确停止纵向主列表则必须交给JS控制原因下一节详细说。再看「怎么停下来」。跑马灯抽奖的停止不是瞬间刹车镜头语言应该像汽车减速进站先快速跑过一段路再逐渐慢下来最后精确卡在某一条目标线上。这意味着滚动速度必须是一条有起伏的曲线而且终点一定要提前确定好。很多初学者在这里犯错——边滚动边随机跑到哪算哪结果经常停在两行名字的缝隙中间非常尴尬。正确思路是反转过来先随机出中奖结果再让动画「自然地走过去」。这个思路看着反直觉但它是后面所有动画逻辑的地基。最后是「停下来之后」。「停」本身不是终点让全场看到谁中了才是终点。中奖那一行要放大、高亮其他行压暗必要时弹一个展示头像和姓名的浮层把大屏上的注意力一次性聚过去。这部分实现难度不大但它直接决定了观众对「这次抽奖是否成立」的感知。我见过不少页面滚动做得不错停下来的瞬间却草草收场整体效果立刻就掉了一档。如果把这三个问题按优先级排我的经验是停止控制排在第一位滚动过程其次高亮反馈再其次。滚动稍微朴素一点没关系停得稳、停得准才是硬指标。如果这个页面是作为网页设计作业交上去我还会建议在基础之上加一点自己的东西。比如参与名单改成从接口拉取、按奖品等级给不同卡片换颜色、加一个「再来一局」按钮或者把抽奖历史记录显示在页面底部。同样是跑马灯抽奖多了这些功能点完成度就不在一个级别。不过前提是先把滚动、停止、反馈这三件事做扎实特色功能才有意义。2. 为什么我不用marquee标签老方案与新方案的真实差距先回答一个每个跑马灯教程都绕不开的问题为什么不用marquee标签这个标签天生就是干滚动这行的它诞生于RSS还流行的年代在当时确实很通用很多人写网页时包一层标签内容自己就跑起来了。但到了跑马灯抽奖这个需求上它有四个绕不开的硬伤而且每一个都碰在痛点上。第一规范上已经废弃。新项目里用marquee代码质量先打一个问号网页设计作业里出现它更是容易被挑毛病。第二只能匀速滚动。抽奖要的是「先快后慢再停稳」marquee根本没有速度曲线这个概念你甚至没法让它缓慢地降速停住。第三终点不可控。你没法告诉它「停在第三个名字上」更没法在滚动过程中根据抽奖结果动态改变目标。第四样式兼容不干净。不同浏览器对marquee的默认表现不完全一致字体、间距、对齐方式都容易出偏差为了抹平这些差异要写不少兼容代码性价比很低。这些硬伤直接指向一个结论跑马灯抽奖的核心滚动不能靠marquee必须换方案。业界常见的路线有三条CSS animation、Canvas、requestAnimationFrame。CSS animation适合做装饰性循环动画比如顶部横向滚动那条祝福语配置好动画时长和循环次数就能跑浏览器还会自动优化性能但遇到「随时打断、根据结果改变终点」的抽奖场景用CSS animation去控制就非常别扭你得动态修改动画时长、监听动画结束事件还要处理帧率波动带来的误差。Canvas适合大量粒子和复杂图形用来滚动名字属于大材小用需要额外维护一整套绘制循环把简单问题搞复杂了。所以对于抽奖核心滚动我更推荐用requestAnimationFrame逐帧驱动位移。它的工作机制很直接每一帧都拿到当前的时间戳配合缓动函数算出一个精确的目标位移然后设置到元素上。这意味着任何一帧你都可以改写目标和方向想快就快、想慢就慢终点也能精确到像素。下面这张表是我在做选型时给自己列的对比写代码之前先看一遍能少走不少弯路。方案优势劣势适合场景marquee写起来最快已废弃、匀速、终点不可控演示性质的简单滚动CSS animation性能好、语法简洁精确停止和动态打断麻烦装饰性循环动画Canvas图像表现力强实现复杂、文本绘制成本高粒子、图形特效requestAnimationFrame逐帧可控、终点精确需要自己管理时间和位移抽奖核心滚动单看表格好像CSS animation也能凑合但我实测下来发现一个很现实的问题抽奖这种交互动画随时可能因为「多抽一轮」「更换名单」「现场临时调整奖品」被打断。requestAnimationFrame给的是逐帧控制权我可以在任意一帧改变目标就算中途换名单、改奖品逻辑也不会乱。CSS animation遇到这种情况往往要先取消当前过渡再重新开启取消瞬间很容易闪烁。对活动页面来说这种「随时可控」的属性比代码简洁重要得多。所以我最终选的是「CSS辅助 requestAnimationFrame驱动核心滚动」的组合装饰逻辑用CSS真正决定谁中奖的逻辑交给JS。3. 从名单到滚动列表DOM结构、无缝滚动与居中偏移计算技术路线定下来之后第一步是搭结构。名单数据先准备成一个数组我习惯叫names正式项目里可以改成接口请求。如果活动分多批抽奖建议直接按批次分组每一组单独维护一个数组后面处理「已中奖者不再参与」时会方便很多。数据这一层不要偷懒把所有参与者信息都准备好姓名是必须的头像和编号可以留空但字段要提前定义好避免后续要加时再大规模改渲染逻辑。DOM结构上核心就是「一个视口 一个滚动的列表」。视口是外层容器固定高度并隐藏溢出内容用户通过它只能看到窗口内的那几行名字列表是真正移动的元素通过transform做位移。每一条名字放在一个li里固定高度和间距。为了方便后续计算我建议把「单条卡片高度」和「卡片间距」设计成固定值最好用一个CSS变量统一管理JS读取同一份值来算偏移量。这样可以避免一个老大难问题CSS里改了个间距JS里没同步结果停止位置全部错位。div classlottery-wrap div classmarquee-viewport idviewport ul classmarquee-list idlist/ul /div button idbtn classlottery-btn typebutton开始抽奖/button /div:root { --item-height: 72px; /* 64px卡片 8px间距 */ } .marquee-viewport { overflow: hidden; height: calc(var(--item-height) * 3); border-radius: 12px; background: #f7f7f7; } .marquee-list { will-change: transform; } .marquee-item { height: 64px; margin-bottom: 8px; display: flex; align-items: center; justify-content: center; font-size: 22px; background: #fff; border-radius: 10px; }滚动的关键细节是无缝循环。如果列表滚到末尾就停下来下一轮开始时再拉回头部用户会明显看到「跳一下」。解决办法是渲染时将完整名单重复一份第一轮从开头滚到第一份末尾需要继续时瞬间把位移重置为第二份开头的偏移值。因为两段内容一模一样用户根本察觉不到重置视觉上就是无限循环。这里有个小经验视口高度一定要做成「奇数个卡片高」比如3个或5个。这样最终停稳时中线正好落在某一行的中心视觉重心是居中的配合高亮效果更自然。如果做成偶数个停止时总是卡在两行中间怎么调都别扭。接下来是偏移量计算这个细节最容易写错。很多人会直接拿「目标索引 × 卡片总高度」当成目标偏移量但这样算出来停下来的位置是视口最顶部那一行而不是中间那一行。要让中奖者停在视口正中必须把「居中的位置偏移」补偿进去。举例来说视口显示3个卡片中间卡片是列表里的第2个位置那么目标偏移量就是(目标索引 - 1) × itemHeight。如果视口显示5个卡片就要减2。这个补偿值看起来只是减个1减个2但真容易漏漏掉的后果是停止位置偏上中奖者和预期的不一致。还有一点容易被忽略名单数量太少时滚动过程没有悬念感两秒就走完了。为了提升迷惑性可以在正式名单之外加一些装饰行比如活动slogan、「敬请期待」之类的占位文字和真实名字混在一起滚动。这样就算现场只有十几个参与者大屏上也能保持一种「名单源源不断」的滚动感。我自己的做法是在数组开头和结尾各追加2到3条装饰项既不影响中奖逻辑滚动起来又显得饱满。中奖索引计算时跳过这些装饰项就可以了逻辑上只是加个偏移因子。4. 抽奖引擎与减速动画先定结果再走向结果抽奖引擎是整页的核心但它的思路比大部分人想得简单点按钮时先随机生成一个中奖索引然后启动动画让滚动列表从当前偏移量移动到目标偏移量。重点全在「怎么移动」这段动画上。这个顺序不能反——如果边滚动边随机就会遇到「动画走了但目标不够远」或者「目标算好了结果又需要重新随机」的尴尬问题。先定结果再做动画整个过程是确定性的任何一帧都清楚自己在往哪里去调试时也好定位问题。我用的缓动函数是easeOutQuart它的特点是前期速度极快、后段减速非常明显正好对应「名单刷屏滚过、最后一下一格一格停住」的现场节奏。整体动画时长我控制在4到5秒之间。短于3秒观众还没进入状态就出结果了悬念感不够长于6秒现场会有人开始聊天注意力就散了。4到5秒是一个经过多次实测的甜点区间。起始阶段位移变化要快让人名不断刷过最后那半秒要慢到「隔一会儿才走一格」全场屏住呼吸的效果就在这一段。function easeOutQuart(t) { return 1 - Math.pow(1 - t, 4); } function startLottery() { if (running) return; running true; const targetIndex Math.floor(Math.random() * names.length); const targetOffset (targetIndex - 1) * itemHeight; const startOffset currentOffset; const distance targetOffset - startOffset; const duration 4000 Math.random() * 1000; const startTime performance.now(); function tick(now) { const progress Math.min((now - startTime) / duration, 1); const eased easeOutQuart(progress); currentOffset Math.round(startOffset distance * eased); list.style.transform translate3d(0, - currentOffset px, 0); if (progress 1) { requestAnimationFrame(tick); } else { running false; highlightWinner(targetIndex); } } requestAnimationFrame(tick); }代码里直接用到的几个变量再解释一遍方便你直接套到自己项目里names是名单数组itemHeight是单条卡片加间距的总高度要和CSS里的--item-height保持一致currentOffset记录当前位移值running是状态锁。targetIndex是本次中奖者的索引targetOffset通过(targetIndex - 1) * itemHeight算出的是中奖者「居中」所需的最终位移。performance.now()拿的是高精度时间戳比Date.now()更适合做动画计时起始和结束的时间差更精准。这里有一个值得说透的取舍为什么不直接用CSS的transition配合缓动函数做减速transition也能配置cubic-bezier缓动表现和现在的方案差不多。但问题在于transition的中途打断能力很弱。如果动画跑到一半需要临时换目标用transition需要先取消当前过渡再重新设置取消的瞬间容易闪烁还要额外监听 transitionend 事件。requestAnimationFrame则可以在任何一帧直接改写 targetOffset 和方向逻辑上就是一个「下一个目标重新计算」的问题。对活动页面来说容错率高的方案就是好方案我最终选了后者付出的一点成本只是手动维护位移和状态。还有一个必须处理的细节防连点。抽奖进行中如果再次触发开始按钮会造成多个动画同时跑列表直接乱掉。我在startLottery开头用running标志位做拦截动画结束并完成高亮之后才释放锁。这里分享一个实测教训在移动端click事件有可能延迟几百毫秒触发如果用户连续快速点击第二次点击进来时锁可能还没来得及生效。所以我建议用pointerdown替代click或者至少在pointerdown阶段就上锁避免「锁还没生效第二次触发已经进来了」。如果做多轮抽奖中奖结果的过滤时机也要想清楚。我建议在动画结束、高亮显示之后再把中奖者从names数组里移除。不要在点击按钮的那一刻就移除否则名单滚动过程中会「少了几个人」迷惑性和参与感都会下降。等这一轮的结果展示完毕再更新数组和重新渲染列表下一轮就从剩余的人里抽。这样既保证了公平性又让整个互动过程看起来完整。5. 停下来之后的仪式感以及实测中踩过的坑抽奖页面的体验一半看滚动过程另一半看停止反馈。名单停住那一刻如果画面就静止在那观众会觉得「这就完了」——缺一个明确的、有分量的结果反馈。我的做法是给中奖那一行加一个active类放大、变色、加光晕同时把相邻行压暗让所有人的视线瞬间聚焦到中奖名字上。高亮的实现不需要JS反复改样式加类名就行剩下的交给CSS过渡。.marquee-item.active { transform: scale(1.15); background: #ffe5a3; color: #7a4b00; box-shadow: 0 0 30px rgba(255, 180, 0, 0.6); transition: transform 0.3s, background 0.3s, box-shadow 0.3s; }高亮不是终点还可以再补一层「停止共振」。比如在停止瞬间弹出中奖者的大图浮层或者加一声清脆的提示音。我的经验是这些反馈越简洁越好活动大屏视觉信息已经很密集动效太多反而抓不住重点。声音最好单独做一个开关现场调试音量和麦克风时能直接关掉不然测试阶段会被提示音烦死。如果你做的是网页设计作业可以在这里加一个自定义的CSS动效比如从屏幕边缘飞入一个祝贺横幅既有设计感又不复杂。然后是我实测过程中踩过的坑按出现频率从高到低排各位做的时候可以提前规避。第一个坑是文字模糊。用transform做位移时如果偏移量带小数比如translate3d(0, -63.4px, 0)浏览器做像素采样时会把半像素处的内容做模糊插值字体边缘看起来就是糊的。减速阶段偏移量变化很小小数出现得尤其频繁不处理的话停在最慢的时候反而是最糊的。解决办法是每帧计算后做一次取整currentOffset Math.round(...)。这一行能解决九成「字怎么发虚」的问题。第二个坑是滚动到底部时的重置跳跃。无缝滚动虽然用克隆解决了循环问题但如果视口高度和卡片总高度对不齐重置那一瞬间边缘会「抖」一下。对不齐的根源通常是视口高度设成了calc(var(--item-height) * 3)但列表底部还混入了额外的margin或padding。建议把视口高度设置成奇数个卡片高列表内部不另加边距顶部和底部各留安全间距。测试时反复盯着底部边缘滚几轮确认没有跳变再算过关。第三个坑是重复中奖。多轮抽奖里如果不对已中奖名单做处理同一个名字很可能被抽上去两次现场气氛会很尴尬。前面说了过滤时机放在动画结束之后这里再补充一个细节从names数组移除中奖者后要同步重算偏移量。如果你用(targetIndex - 1) * itemHeight来定位而数组长度已经变了decorative item的偏移要重新算不然第二次抽奖停止的位置会偏。稳妥做法是每次更新数组后统一重新初始化滚动列表。第四个坑是后台标签页动画暂停。requestAnimationFrame在浏览器标签页切到后台时会暂停如果演示时切走页面再切回来名字会停在半路一动不动看起来就像卡死了。这个特性对现场大屏几乎没有影响但如果你需要录屏、远程演示或者用来部署在触屏一体机里被人误切到后台就要提前知道这个行为。真遇到的话可以在页面重新可见时检测动画状态重新计算剩余时长再继续。这个逻辑不算复杂但能避免演示现场的尴尬。如果你也在做类似的跑马灯抽奖页面我建议先把小数取整和防连点这两件事处理掉它们直接决定页面「稳不稳」。其他的等你在现场听到第一轮欢呼就会觉得之前反复调曲线、调时间、测边界都值了。本文还有配套的精品资源点击获取