简介:面向需要实现全屏视频背景的网页开发者与前端学习者,这套示例围绕 HTML video 元素与 CSS 全屏定位、object-fit: cover、媒体查询响应式适配展开,可用于品牌首页、活动页或作品集等沉浸式视觉场景,也适合用来对照理解视频比例裁剪、移动端显示与性能取舍。压缩包共 3 个文件,包含 1 个 mp4 示例视频、1 个 html 页面和 1 个 css 样式文件,整体约 4.11MB,体量轻便,便于快速查看结构与效果。现有 5377 人学习,说明该主题在实战中较受关注。读者可从中获得一套可直接参考和运行的全屏视频背景演示,涵盖视频源引入、铺满视口、保持原始比例以及小屏适配等关键实现,并借由示例理解低带宽备选与文件压缩等体验优化思路,适合作为前端视觉特效入门的实践样本。
1. 二十行 css html 就能铺满整屏,出问题的往往不是这二十行
搜索“css html 视频背景”,结果几乎一样:一个<video>标签,加一句object-fit: cover,很快就能写完。本地打开画面很漂亮,缩窗口也不会变形,容易觉得这活很简单。
某开发者拿这套写法为一个品牌活动页做首屏。桌面端一切正常,设计稿还原得很好。交付前一天,测试同学用手机打开后却是一片黑;换另一台手机又正常。现场排查半小时才发现,那台机器开着低电量模式,系统直接拒绝了自动播放。
这个资源是一份能直接打开的全屏视频背景页面,由 HTML、CSS 和少量不依赖第三方库的 JS 组成,处理了自动播放被拒绝后的降级、移动端等比裁切铺满、遮罩层压暗以保证文字可读、首帧 poster 防白闪这几件事。它适合做品牌首页、活动落地页、产品介绍首屏的前端,也适合想彻底搞明白 autoplay 策略和 video 全屏裁切的人。
2. 项目骨架与技术选型:三层结构怎么分、素材怎么出
2.1 目录结构与每个文件的分工
这页面只有一个首屏,内容区加起来不到三十行文字,引入框架没有收益。用打包器构建出的运行时,加上一个组件库,首屏加载的资源就会从几十 KB 涨到几百 KB,而视频本身已经在连接上占用了带宽。纯静态的 HTML、CSS 和 JS 放到静态托管上就能跑,缓存策略也好配。
目录长这样:
| 路径 | 作用 | 改动频率 |
|---|---|---|
index.html | 页面骨架、video 标签、内容区 DOM | 低 |
css/reset.css | 去掉默认 margin、统一盒模型 | 几乎不改 |
css/video-bg.css | 视频层、遮罩层、内容层全部样式 | 中 |
js/video-bg.js | 自动播放兜底、降级开关、移动端换图 | 中 |
media/bg-1080.mp4 | H.264 主源,兼容性兜底 | 换素材时改 |
media/bg-1080.webm | VP9 版本,体积通常小 30% 左右 | 换素材时改 |
media/poster.jpg | 首帧图,视频没就绪前顶着 | 换素材时改 |
CSS 拆成 reset 和业务两份,为了后续这个首屏被复制到别的活动页时,能把video-bg.css整块搬走,而不用连 reset 一起搬,避免把别人页面的样式冲掉。JS 不引任何库,一个 IIFE 包起来,挂到页面上就行。
本地跑不要直接双击index.html。file://协议下部分浏览器会把 video 当跨域资源处理,控制台一片报错,排查半天以为是代码问题。起个静态服务最省事:
# 方式一:Python 自带 python -m http.server 8080 # 方式二:Node 环境 npx serve . -l 8080打开http://localhost:8080看效果,改完 CSS 刷新即可,不需要构建步骤。
2.2 三层结构:视频层、遮罩层、内容层
页面在视觉上是“视频、一层半透明黑、文字”,但 DOM 上不能真的把遮罩盖在文字上,否则文字选不中、按钮点不了。正确的分层是三个并列的兄弟节点,用层叠上下文控制绘制顺序。
层级规划如下:
| 层 | 节点 | z-index | 关键样式 |
|---|---|---|---|
| 视频层 | .video-bg | 0 | position: fixed、object-fit: cover、pointer-events: none |
| 遮罩层 | .hero__mask | 1 | position: absolute、inset: 0、渐变背景 |
| 内容层 | .hero__content | 2 | position: relative、flex 居中、max-width |
父容器.hero上加一句isolation: isolate,作用是强制建立一个独立的层叠上下文。这句是血泪经验:早期版本图省事,把视频写成z-index: -1,桌面端没事,某些移动端浏览器上整块视频直接跑到body背景后面,页面只剩白底黑字,看起来像视频没加载,其实是层级被吃掉了。
提示:只要用了负数 z-index,就一定在最近的可视父容器上加
isolation: isolate,或者干脆改用 0/1/2 正向分级,后者更省心。
这样分层还有一个好处:内容层的按钮天然在遮罩之上,不需要额外给遮罩加pointer-events: none,遮罩本身不接收事件也不影响下面的视频,因为视频层自己就设了pointer-events: none。
2.3 视频素材的规格约定
代码再好,素材给错了照样出问题。这个项目对素材有一套硬约定,不是审美偏好,是性能账。
| 参数 | 建议值 | 原因 |
|---|---|---|
| 分辨率 | 1920×1080 | 再高对首屏没有视觉收益,解码压力翻倍 |
| 时长 | 8~15 秒,无缝循环 | 太长文件大,接缝明显会被一眼看穿 |
| 帧率 | 25 或 30fps | 60fps 对背景视频是浪费 |
| 码率 | 2~4 Mbps | 1080p 背景视频的舒适区 |
| 文件体积 | 单文件 ≤ 4MB | 超过这个数,弱网用户首屏基本看不到视频 |
| 编码 | H.264(mp4)+ VP9(webm) | 前者兼容性兜底,后者省流量 |
| 首帧 | 必须是好看的画面 | poster 直接取第一帧,首帧是黑场就等于白闪 |
循环接缝这块有个小技巧:剪辑时把结尾往回多留 1~2 秒,让它和开头重叠淡入,播放器loop重播时不容易看出跳帧。另外视频里不要放细密条纹、快速横摇镜头,这类画面码率压不下去,压下去了也会出现明显色块,而背景视频糊了比不动更难看。
素材按这套规格出,后面所有代码才有意义。很多“视频背景卡顿”的锅最后都落在素材上,代码本身没有错。
3. 核心代码拆解:video 标签怎么写才能全屏且自动播放
3.1 HTML 结构:autoplay、muted、playsinline 一个都不能少
先看 video 标签本体,这几行是整个项目最容易翻车的地方:
<!-- 视频层:关键属性全写在标签上,别指望 JS 事后补 --> <video id="bgVideo" class="video-bg" autoplay muted loop playsinline webkit-playsinline preload="auto" poster="./media/poster.jpg" disablepictureinpicture aria-hidden="true" tabindex="-1"> <source src="./media/bg-1080.webm" type="video/webm"> <source src="./media/bg-1080.mp4" type="video/mp4"> 当前浏览器不支持 video 标签,这里会退化为 poster 图。 </video>逐个说清楚为什么这么写:
autoplay不用解释,但它的生效前提是视频被判定为“静音”。muted必须以 HTML 属性形式存在,不能只靠 JS 里的video.muted = true。部分浏览器在解析标签阶段就决定了要不要放行自动播放,等 JS 执行到那一行,策略判定早就做完了,再设也没用。
playsinline和webkit-playsinline是给 iOS 的。少了这两个属性,视频一播就强制全屏接管,用户看到的是播放器而不是你的页面。带前缀的那个是给老版本的兼容写法,成本一行,加上没坏处。
preload="auto"表示希望浏览器尽快把视频拉下来。如果项目对流量敏感,比如面向移动端用户为主的落地页,改成metadata只预取头部信息,等首屏文字渲染完再靠 JS 触发加载,首屏指标会好看不少,代价是视频出现得慢一点。
poster是第一道视觉兜底。视频下载和解码需要时间,这段空窗期如果什么都没有,用户看到的就是一块黑或一块白,主观感受是“页面坏了”。给一张和首帧一致的图,视觉上是连续的。
disablepictureinpicture挡掉部分移动端浏览器自动加的画中画按钮,背景视频弹出一个悬浮小窗会很难看。aria-hidden="true"和tabindex="-1"是给读屏软件和键盘 Tab 用的,视频对内容理解没有贡献,不该被聚焦。
<source>的顺序有讲究:webm 放前面,支持的浏览器命中它就停,不支持的继续往下找 mp4。反过来写,mp4 永远先命中,webm 等于白转。
3.2 CSS 铺满:object-fit 与兼容兜底
现代浏览器一行搞定:
/* 基础版:object-fit 等比裁切铺满,不拉伸 */ .video-bg { position: fixed; inset: 0; /* top / right / bottom / left 全 0 */ width: 100%; height: 100%; object-fit: cover; /* 关键:裁切而不是拉伸 */ object-position: center center; z-index: 0; pointer-events: none; /* 不拦截点击,按钮才点得到 */ background: #000; /* 视频解码前是黑底,不是白闪 */ }object-fit: cover的含义是保持视频原始宽高比,缩放到刚好覆盖整个盒子,超出部分裁掉。用width: 100%; height: 100%硬拉,画面会变形,人像脸被压扁,这个错误在评审时一眼就能看出来。object-position: center center决定从哪个位置裁,默认居中,如果视频主体偏左,改成30% center能把主体留住。
兼容兜底版本给老内核用,写法是传统的绝对定位加反向位移:
/* 兜底:不支持 object-fit 的环境,靠 min-width/min-height 撑满再居中裁切 */ .video-wrap { position: fixed; inset: 0; overflow: hidden; /* 撑出去的部分在这里被裁掉 */ z-index: 0; pointer-events: none; } .video-wrap video { position: absolute; top: 50%; left: 50%; min-width: 100%; min-height: 100%; width: auto; height: auto; transform: translate(-50%, -50%); }原理是min-width和min-height同时生效时,谁的比例先满足谁说了算,另一边按原始比例自动撑开,结果就是“至少铺满、可能超出”,再由父级的overflow: hidden裁掉超出部分。translate(-50%, -50%)把左上角对齐改成中心对齐,保证裁切是均匀的。
两个版本可以共存:默认用外层.video-wrap包住,内层 video 写object-fit: cover,支持的环境直接用,不支持的环境自动走min-width那套,不需要 UA 判断。
3.3 JS 兜底:play() 的 Promise 与失败降级
自动播放被拒绝是常态,不是异常。低电量模式、省流模式、用户关掉了媒体自动播放、页面在后台标签页打开,任何一个都会让autoplay失效。所以要有一个明确的降级分支:
(function () { var video = document.getElementById('bgVideo'); if (!video) return; // 双保险:再赋一次,兜住个别浏览器读标签属性不生效的情况 video.muted = true; video.defaultMuted = true; video.setAttribute('muted', ''); // play() 返回 Promise,被策略拒绝时走 catch var p = video.play(); if (p !== undefined) { p.catch(function (err) { // NotAllowedError:没有用户交互;NotSupportedError:格式不被支持 console.warn('[bg-video] autoplay blocked:', err.name); document.documentElement.classList.add('video-fallback'); video.style.visibility = 'hidden'; // 露出底下的 poster / 纯色底 }); } // 用户第一次触摸或点击时再试一次,成功或失败都解绑 function retry() { video.play().catch(function () {}); window.removeEventListener('touchstart', retry); window.removeEventListener('click', retry); } window.addEventListener('touchstart', retry, { passive: true }); window.addEventListener('click', retry); })();这段代码有三处值得展开。第一,play()在新版浏览器里返回 Promise,老浏览器返回undefined,所以先判空再挂catch,否则老环境会直接抛 TypeError。第二,catch里做的是“隐藏视频”,而不是“显示一个播放按钮”——背景视频不适合让用户点,藏掉之后下面的poster或纯色底接上,页面看起来仍然完整。第三,retry绑在touchstart和click上,用的是{ passive: true },不阻塞滚动,用户一旦有任何交互就再争取一次机会,成功后立刻解绑,避免每次点击都在那儿调play()。
配合这段 JS,CSS 里加一条降级态:
/* 自动播放失败时的降级:用渐变底代替视频,文字依然可读 */ .video-fallback .video-bg { display: none; } .video-fallback .hero { background: #0b0d12 url('../media/poster.jpg') center / cover no-repeat; }这个组合拳的实战价值在于:视频没播起来,用户也不会觉得页面坏了,最多是“这个页面图挺大”。
4. 遮罩与排版:让文字在动的画面上依然可读
4.1 遮罩层的三种做法与取舍
视频背景最大的可用性问题是文字压不住画面。一条亮色天空扫过去,白字直接消失。解决办法是在视频和文字之间加一层遮罩。
第一种是纯色半透明,background: rgba(0, 0, 0, 0.45),最简单也最可控,缺点是画面整体变闷,视频的质感被压掉一截。
第二种是渐变,上浅下深或上深下浅:
.hero__mask { position: absolute; inset: 0; z-index: 1; /* 在视频之上、内容之下 */ background: linear-gradient( 180deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.55) 100% ); pointer-events: none; }文字通常放在画面中下部,底部加深能托住文字,顶部保持通透让视频透气。这种是最常用的折中,实际项目里八成选它。
第三种是backdrop-filter: blur(8px)做模糊玻璃效果。视觉上确实高级,但它是实时模糊,等于让 GPU 在每一帧都对全屏画面做一次卷积,中低端手机上帧率掉得很明显。我一般只在遮罩面积小的时候用,比如弹窗、卡片,全屏背景慎用。
还有个小手法:给文字加text-shadow: 0 2px 12px rgba(0, 0, 0, 0.35)。阴影不改变画面整体亮度,只在字边缘制造对比,和遮罩叠加使用,文字清晰度会更稳。
4.2 内容区的层级与点击穿透
内容层的结构很朴素,但每条样式都有原因:
.hero__content { position: relative; z-index: 2; max-width: 720px; /* 控制行宽,长文本一行别超过 40 个汉字 */ margin: 0 auto; padding: 0 24px; min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; color: #fff; }max-width: 720px这个限制经常被忽略。全屏页面上文字一旦铺满整行,用户眼睛要横跨一整个屏幕,读起来很累,视觉上也不像设计稿。720px 是中文场景下一个比较舒服的宽度。
justify-content: center让内容垂直居中,但内容高度如果超过一屏,居中会导致顶部溢出被裁掉。这种情况下改成justify-content: flex-start加padding-top: 20vh,或者用min-height而不是height,让容器能被内容撑高,配合整页滚动。
点击穿透这块要特别检查:视频层和遮罩层都设了pointer-events: none,内容层的按钮才能正常触发。如果哪天视频上想加一个“暂停背景”的小图标,记得把那个按钮单独放到内容层,或者给视频层开pointer-events: auto再给按钮单独设z-index,不要让整层都变成事件拦截器。
4.3 响应式与 100dvh:手机地址栏那点事
height: 100vh在移动端浏览器上是个经典坑。vh按浏览器最大可视高度算,地址栏收起和展开时可视区域变化,但vh不变,结果就是首屏底部被地址栏吃掉一截,或者留出一条白边。新版浏览器支持dvh(动态视口单位),会跟着地址栏实时变:
.hero { min-height: 100vh; /* 老环境先兜住 */ min-height: 100dvh; /* 支持的浏览器覆盖掉上一行 */ }两行写在一起,不支持的浏览器忽略第二行,支持的用第二行,是标准的渐进增强写法。
字号用clamp()做流体排版,省掉一堆断点:
.hero__content h1 { font-size: clamp(28px, 6vw, 64px); line-height: 1.15; letter-spacing: -0.02em; margin: 0 0 16px; } .hero__content p { font-size: clamp(14px, 1.6vw, 18px); line-height: 1.8; margin: 0 0 32px; } /* 小屏上把留白收紧,别让文字被挤成三行 */ @media (max-width: 768px) { .hero__content { padding: 0 20px; } .hero__content p { margin-bottom: 24px; } }clamp(最小值, 弹性值, 最大值)的意思是:视口宽度乘以系数作为弹性值,低于最小值取最小值,高于最大值取最大值。6vw在 375px 宽的手机上约等于 22.5px,会被 28px 兜住;在 1440px 的桌面上约等于 86px,会被 64px 封顶。中文标题还要注意行高,1.15 在高对比背景上比较紧凑,如果标题换行了,可以把line-height提到 1.25。
移动端还有一件事:视频码率该降。同一条 4MB 的 1080p 素材,在 4G 弱网下加载十几秒很正常。稳妥的做法是媒体查询或者matchMedia判断屏幕宽度,小于 768px 时不加载视频,直接用 poster 静态图,省流量也让首屏更快。这个开关放在下一章的组件化里更合适。
5. 避坑与排查:黑屏、不自动播放、画面拉伸的五个真实故障
5.1 播放与兼容类故障
故障一:iOS 上打开是黑的,只有 poster 图,控制台报 NotAllowedError
现象:桌面端正常,iPhone 上首次进入页面视频不播,手动滑一下或者点一下才开始。
原因:系统级的自动播放限制。低电量模式、蜂窝网络下开启的省流模式,都会直接拒绝任何非用户触发的播放。另外如果muted只写在 JS 里,解析阶段浏览器认为这视频有声音,也会拒绝。
解决:muted、playsinline全部写在 HTML 属性上;play()挂catch做降级;监听一次touchstart重试。这三步做完,能覆盖九成以上场景,剩下的是系统真的不让播,那就不播,让 poster 顶上。
故障二:移动端视频上下出现大黑边,画面被压扁或者只显示一部分
现象:桌面端铺满,手机上要么上下黑边,要么人脸被横向拉宽。
原因:多半是用了width: 100%; height: 100%硬拉,或者父容器没设overflow: hidden,视频按原始比例撑出去但没裁。也有环境对object-fit支持不完整。
解决:改用.video-wrap+min-width/min-height+translate(-50%, -50%)这套兜底方案,父级overflow: hidden一定要加。素材按 16:9 出,横屏素材竖屏铺满时左右裁掉的部分要提前在剪辑里避开主体。
故障三:视频能播,但每隔十几秒卡一下,尤其在中低端安卓机上
现象:循环播放时周期性掉帧,滚动页面时更明显。
原因:素材码率过高或者分辨率是 4K,解码压力大;也可能是同时存在多个 video 元素,或者页面里还有别的高开销动画在抢 GPU。
解决:素材降到 1080p、2~4 Mbps;一条页面只留一个背景 video;循环接缝处不要有剧烈画面变化。另外检查一下有没有给 video 加filter或transform: scale()这类会触发重新合成的样式,能不加就不加。
5.2 布局与性能类故障
故障四:页面加载出来一片黑,文字和按钮全看不见
现象:DOM 里有内容,但视觉上只有黑色背景,滚动一下才偶尔露出文字。
原因:视频用了z-index: -1,而父容器没有建立层叠上下文,负层级元素跑到了body背景之后,把同级的遮罩和内容一起压在下面。另一种可能是遮罩的透明度写成了1。
解决:给父容器加isolation: isolate,或者把层级改成 0/1/2 正数分级。改完在浏览器开发者工具的图层面板里看一眼顺序,视频在最底、遮罩居中、内容最上,就对了。
故障五:首屏白闪一两秒,Lighthouse 的 LCP 跑到 4 秒以上
现象:打开页面先白一下,然后视频突然出现。
原因:视频文件太大(见过 30MB 的),preload="auto"在首屏就和关键 CSS/字体抢带宽;没有 poster,解码完成前没有任何东西可画。
解决:素材压到 4MB 以内;poster 用视频首帧导出,格式用 jpg 或 webp,控制在 100KB 左右;video 元素加background: #000避免白底;如果带宽实在紧张,把preload改成metadata,用 JS 在window.load之后再设video.src触发加载。改完用开发者工具的网络面板限速到“慢速 3G”再测一遍,这是最接近真实用户的一次检验。
注意:这五条里,故障一和故障五在桌面端开发环境几乎不可能复现,必须拿真机、开弱网才能看到,别指望本地 Chrome 帮你发现问题。
6. 二次开发:把它改成可配置的视频背景组件
复制粘贴到第三个活动页之后,我开始烦了——每换一次素材就要改 HTML、改 CSS 里的路径、改移动端判断。于是把它收成一个函数,配置项外置:
// createVideoBg.js —— 传容器和配置,返回可控实例 export function createVideoBg(container, options = {}) { const opt = Object.assign({ mp4: '', // 必填,主源 webm: '', // 可选,命中优先 poster: '', // 首帧图,降级用 overlay: 0.45, // 遮罩透明度 0~1 mobileBreakpoint: 768, // 小于该宽度不加载视频 mobileImage: '', // 移动端静态图 respectReducedMotion: true // 尊重系统"减弱动态效果" }, options); const isMobile = window.matchMedia(`(max-width: ${opt.mobileBreakpoint}px)`).matches; const noMotion = opt.respectReducedMotion && window.matchMedia('(prefers-reduced-motion: reduce)').matches; // 移动端或用户要求减弱动效:直接走静态图,不下载视频 if (isMobile || noMotion) { container.style.background = `url(${opt.mobileImage || opt.poster}) center / cover no-repeat`; return { play() {}, pause() {}, destroy() {} }; } const video = document.createElement('video'); video.muted = true; video.loop = true; video.playsInline = true; video.preload = 'auto'; video.poster = opt.poster; video.className = 'video-bg'; if (opt.webm) { const s = document.createElement('source'); s.src = opt.webm; s.type = 'video/webm'; video.appendChild(s); } const s2 = document.createElement('source'); s2.src = opt.mp4; s2.type = 'video/mp4'; video.appendChild(s2); container.prepend(video); video.play().catch(() => { video.style.visibility = 'hidden'; }); return { play: () => video.play().catch(() => {}), pause: () => video.pause(), destroy: () => video.remove() }; }配置项里最值得说的是mobileBreakpoint和respectReducedMotion。前者是纯成本考虑:手机上用一个 100KB 的静态图,换掉 4MB 的视频,首屏时间和流量都值。后者是可用性考虑,系统里开了“减弱动态效果”的用户,大概率对满屏晃动的画面不适,这时候不加载视频是尊重,不是偷懒。
暴露play / pause / destroy三个方法,是为了应对单页应用的路由切换。离开页面时调destroy(),把 video 从 DOM 里摘掉,浏览器才会真的停止解码。我见过一个后台系统,五个页面各带一个背景视频,来回切了十几次,内存直接上去几百 MB,就是因为只做了pause()。
改造完的验证清单只有三条,但一条都不能省:真机开低电量模式进一次,确认不播的时候页面依然完整;开发者工具限速到慢速 3G 刷一次,确认首屏不会白屏超过两秒;系统设置里打开“减弱动态效果”再进一次,确认走的是静态图分支。
从那以后我每次交付带视频背景的页面,都会先在低电量模式的真机上打开一次,再断网刷新一次,两关都过了才敢发版。这套习惯救过我两次,一次是 poster 路径写错导致断网全黑,一次是自动播放失败后文字压在亮画面上完全看不清。希望帮到你。
本文还有配套的精品资源,点击获取