Tailwind 综合实战:从零搭一个单文件页面
以单文件 HTML 幻灯片为载体的 Tailwind 综合实战:浏览器直引与本地构建取舍、CSS 变量配色板接入 @theme、全屏 slide 容器与 data-active 切换动画、clip-path 几何切割与斜纹网格背景,并与 Reveal.js 方案对照
知识点地图
- 知识类别:Tailwind 综合实战——把模块前序篇的工具类、变体、令牌、视觉特效,落成一个”双击就能打开”的单文件页面。
- 解决什么问题:临时分享、课程演示、活动落地页这类”不需要工程化”的场景,如何用正确的姿势绕过构建步骤;以及幻灯片这种分屏切换页面应有的结构。
- 什么时候用到:社团分享、快速原型、给非前端同事交付演示稿;也是检验前序篇掌握程度的期末测试。
- 本篇是变体引擎与交互状态、变换、滤镜与视觉特效、主题定制与设计令牌的汇合点。
前置知识
- Tailwind CSS 主题定制与设计令牌:会写
@theme令牌。 - Tailwind 变体引擎与交互状态:会用
data-[...]属性变体。 - Tailwind 变换、滤镜与视觉特效:会用 clip-path、repeating-linear-gradient 与 transition。
学习目标
- 能说出浏览器直引(
@tailwindcss/browser)与本地构建各自的边界,并按场景选对。 - 能在无构建的页面里用
<style type="text/tailwindcss">接入@theme配色板。 - 能搭出”全屏 slide + data-active 切换 + cubic-bezier 缓动”的幻灯片骨架。
- 能把 clip-path 几何切割与双色斜纹网格背景落进页面装饰。
- 能说清手写方案与 Reveal.js 的取舍标准。
0. 从一份真实的社团幻灯片说起
一个社团招新宣讲,需要十来页幻灯片:讲师人手一份、现场电脑环境未知、还要发到群里让大家自己翻。约束倒推出技术选型——单个 HTML 文件,双击即开,不发依赖。这正是 Tailwind 的舒适区:所有样式都在类名里,浏览器直引后连 CSS 文件都不用发。
本篇按这份真实产物的思路从零走一遍:先定载入方式,再接配色板,然后搭全屏 slide 切换骨架,最后落两处几何装饰。走完你就得到了一个可复用的单文件页面模板。
1. 载入方式取舍:直引还是构建
Tailwind v4 提供一个官方浏览器内编译脚本,无需任何构建:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>两种路线的判断表:
| 维度 | 浏览器直引 | 本地构建(vite/CLI) |
|---|---|---|
| 首次可用 | 保存即生效,零配置 | 需要 Node 环境与依赖安装 |
| 首屏开销 | 运行时扫描 DOM 并现场生成 CSS,页面越大越慢 | 构建期产出精简 CSS,只含用到的类 |
| 生产适用 | 官方明确仅供开发/试用,不建议生产 | 生产标准做法 |
| 令牌与自定义 | <style type="text/tailwindcss"> 支持 @theme 等全部 CSS 特性 | 同左 |
| 离线 | 断网即失效(脚本在 CDN) | 产物本地自持 |
选型口诀:给”人手一个文件、随手打开”的场景选直引;给会上线、要打磨的页面选构建。单文件幻灯片的社交属性(群里转发、U 盘拷贝)压倒一切,所以直引;但要承认代价——现场无网就白屏,稳妥做法是同时检查一遍离线场景能否接受。
2. 配色板:CSS 变量接入 @theme
直引页面的令牌写进 <style type="text/tailwindcss">,Tailwind 会像构建模式一样处理它:
<style type="text/tailwindcss">
@theme {
--color-brand: #2563eb;
--color-brand-deep: #1e40af;
--color-accent: #f59e0b;
--color-ink: #0f172a;
}
</style><!-- 令牌立刻变成工具类 -->
<h1 class="text-brand">社团招新</h1>
<div class="bg-brand-deep text-white">深蓝底块</div>讲解:与”散落写死 #2563eb”相比,令牌化之后全页只出现一次色值——临时换主题色只动一处(令牌原理的完整展开见主题定制与设计令牌)。单文件页面虽然小,令牌纪律不该松:色值、缓动曲线这类”审美决策”进 @theme,一次性装饰留给任意值。
3. 骨架:全屏 slide 与 data-active 切换
3.1 结构与状态约定
每页一个 <section>,绝对定位叠满全屏;切换状态不靠 display 硬切,而是”透明 + 位移”的组合动画——进入的页从下方浮上来,离开的页淡出:
<main class="relative h-dvh overflow-hidden bg-ink">
<section
data-slide data-active
class="absolute inset-0 flex flex-col items-center justify-center gap-6
opacity-0 translate-y-8 pointer-events-none
transition-[opacity,translate] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]
data-[active]:opacity-100 data-[active]:translate-y-0 data-[active]:pointer-events-auto">
<h1 class="text-5xl font-black text-white">网页三件套</h1>
<p class="text-lg text-gray-300">骨架、皮肤与灵魂</p>
</section>
<section
data-slide
class="absolute inset-0 ... 同上">
<h2 class="text-3xl font-bold text-white">HTML:网页的骨架</h2>
</section>
</main>逐段拆解为什么这样写:
absolute inset-0叠放:所有页同位叠放,切换发生在同一坐标上,天然支持交叉过渡;h-dvh(动态视口高度)比h-screen稳,移动端地址栏收展不留白。opacity-0 translate-y-8作默认态:不写的页处于”待命”状态——透明、略下移、pointer-events-none不拦截点击。data-[active]:三连:激活页恢复可见、归位、恢复可点。JS 只做一件事——切换data-active属性(属性变体的完整机制见变体引擎与交互状态第 2.2 节)。transition-[opacity,translate]:只过渡两个合成属性;ease-[cubic-bezier(0.22,1,0.36,1)]是一份”先快后缓带一点回稳”的曲线,比默认 ease 更有”落座感”,与上一行的缓动一起属于”审美决策”,也可以提升为--ease-*令牌。
3.2 驱动它的十行 JS
<script>
const slides = [...document.querySelectorAll('[data-slide]')];
let cur = 0;
const show = (i) => {
cur = (i + slides.length) % slides.length;
slides.forEach((s, k) => k === cur ? s.setAttribute('data-active', '') : s.removeAttribute('data-active'));
};
addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight' || e.key === 'PageDown') show(cur + 1);
if (e.key === 'ArrowLeft' || e.key === 'PageUp') show(cur - 1);
});
addEventListener('click', (e) => { if (!e.target.closest('a, button')) show(cur + 1); });
</script>讲解:show() 里的取模让翻页在首尾循环(演示时不会”卡死在最后一页”的尴尬);键盘左右键与”点空白处前进”覆盖了演示的两种操作习惯。注意过渡动画能生效的前提是页元素始终在 DOM 里(只是透明),这正是第 3.1 节不用 display 切换的原因——display 从 none 到可见不产生过渡帧(原理见动画与过渡)。
4. 装饰落地:几何切割与斜纹网格
幻灯片的”设计感”靠两处几何装饰撑起来,都是变换、滤镜与视觉特效里技法的落地。
4.1 clip-path 几何切割色块
<!-- 封面页右下角的几何色块:斜切 + 双色叠加 -->
<div class="absolute bottom-0 right-0 h-48 w-96 bg-brand/20
[clip-path:polygon(100%_0,100%_100%,0_100%)]"></div>
<div class="absolute bottom-0 right-0 h-32 w-72 bg-brand
[clip-path:polygon(100%_0,100%_100%,0_100%)]"></div>讲解:两层同形三角(大而淡、小而实)沿右下角对齐,“同形不同色”的叠加是几何风版面最省力的套路——只写一个 polygon 形状,靠尺寸与透明度做层次。bg-brand/20 的斜杠透明度修饰符让色块与背景”融”而不是”贴”。
4.2 双色斜纹网格背景
内容页的底纹用两个方向的 repeating-linear-gradient 叠加出网格:
<section class="bg-[repeating-linear-gradient(45deg,rgba(37,99,235,0.06)_0,rgba(37,99,235,0.06)_24px,transparent_24px,transparent_48px),repeating-linear-gradient(-45deg,rgba(37,99,235,0.06)_0,rgba(37,99,235,0.06)_24px,transparent_24px,transparent_48px)]">
<h2 class="text-3xl font-bold">HTML:网页的骨架</h2>
</section>讲解:两个渐变一个 45deg、一个 -45deg,交叉形成菱形网格;透明度压到 0.06 只做”能感知但不抢戏”的底纹——装饰的第一纪律是不与内容争夺对比度,正文文字所在区域宁可不要底纹。这里的类名已经长到影响可读性,单文件页面内出现第二处同款时,就把它提升为自定义样式(见第 6 节的 <style> 块)。
5. 对照组:什么时候该用 Reveal.js
同样的内容用 Reveal.js 实现,逐条入场的分片动画是标配:
<!-- Reveal.js 的分片:每个 fragment 依次出现 -->
<section>
<h1>网页三件套</h1>
<p class="fragment fade-up">HTML 是骨架</p>
<p class="fragment fade-up">CSS 是皮肤</p>
<p class="fragment fade-up">JavaScript 是灵魂</p>
</section>两条路线的取舍标准:
| 需求 | 手写单文件 | Reveal.js |
|---|---|---|
| 翻页与全屏排版 | 十几行 JS 够用 | 内建且更完善 |
| 逐条入场(fragment) | 要自己维护序列状态 | 一行 class |
| 演讲者视图 / 备注 | 没有 | 内建 |
| 分发形态 | 单文件、零依赖 | 需带整套库或用其 CDN |
| 定制自由度 | 全部自己的代码 | 受其主题体系约束 |
选型口诀:需要”演讲”的专业设施(分片、备注、缩略图)用 Reveal.js;需要”分发”的轻量形态(群发、挂网、强品牌视觉)手写单文件。两者不冲突——手写方案练的是结构与过渡的基本功,理解了这些,Reveal.js 的每个配置项对你都不再是黑盒。
6. 组装:最小可用模板
把前面各节拼成一份可以直接保存为 deck.html 的骨架(内容页样式省略号处照第 3 节补全):
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>社团招新 · 网页三件套</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@theme {
--color-brand: #2563eb;
--color-brand-deep: #1e40af;
--color-ink: #0f172a;
--ease-seat: cubic-bezier(0.22, 1, 0.36, 1);
}
/* 第二处起复用的底纹:提升为具名类 */
.bg-diagonal-grid {
background-image:
repeating-linear-gradient(45deg, rgb(37 99 235 / 0.06) 0 24px, transparent 24px 48px),
repeating-linear-gradient(-45deg, rgb(37 99 235 / 0.06) 0 24px, transparent 24px 48px);
}
</style>
</head>
<body class="bg-ink font-sans text-white">
<main class="relative h-dvh overflow-hidden">
<!-- 多个 data-slide section:结构见第 3.1 节 -->
</main>
<script>
/* 驱动逻辑:见第 3.2 节 */
</script>
</body>
</html>讲解:这份模板的价值在于边界清晰——@theme 管审美决策,.bg-diagonal-grid 管复用装饰,工具类管一次性排布,十行 JS 管状态。每一层都对应前序篇的一条纪律,单文件不等于单团乱麻。
7. 常见错误与对策
| 错误场景 | 表现 | 原因 | 解决办法 |
|---|---|---|---|
| 离线打开白屏 | 无网环境页面裸奔 | 直引脚本在 CDN | 提前接受该约束,或改为本地构建产物 |
| slide 切换无动画 | 内容瞬间跳变 | 用了 display 切换 | 始终在 DOM,靠 opacity/translate 过渡 |
| 底纹抢戏 | 正文读不清 | 装饰对比度过高 | 底纹透明度压到 0.06 级别 |
| 令牌散落 | 换色要全局搜索 | 色值写死在任意值里 | 审美决策进 @theme |
| 移动端底部留白 | 页面比视口矮一截 | 用了 100vh | 换 h-dvh |
8. 动手实践
任务一:扩展到五页。 在模板上加”招新要求”与”报名方式”两页,报名方式页放一个可点击的链接(注意别被全局”点空白翻页”劫持)。提示:第 3.2 节 JS 里 e.target.closest('a, button') 已排除点击穿透,验证它;再给每页加右下角页码。
任务二:主题一键换肤。 把 @theme 里的 --color-brand 换成另一个色值,确认全部 brand 类跟随;再挑战:给 <html data-theme> 挂两套 brand 值实现蓝橙双主题。提示:参考主题定制与设计令牌第 3 节的 [data-theme] 作用域写法。
任务三:入场升级。 让标题在所在页激活时”逐词浮入”(三个词错开 100ms)。提示:三个 span 各配 transition-all duration-500 ease-seat 与 delay-*,激活态由父页的 data-[active] 触发——想想如何用 data-[active]:[&>span]:opacity-100 一类组合表达”后代响应祖先状态”。
先自己写,再对照参考实现:
任务一参考实现
<footer class="absolute bottom-4 right-6 text-sm text-gray-400"
data-page-num>1 / 5</footer>// show() 里追加一行:把当前页码写进每页的页码角标
const pagers = [...document.querySelectorAll('[data-page-num]')];
pagers.forEach((p) => (p.textContent = `${cur + 1} / ${slides.length}`));链接不被劫持的关键在第 3.2 节的 closest('a, button')——事件先命中链接时直接放行,浏览器走默认导航。这个判断要放在”翻页”分支之前,顺序写反就成了”点链接先翻页”的事故。页码角标同步进 show() 而不是监听别的时机,保证状态只有一个变更入口。
任务二参考实现
<style type="text/tailwindcss">
@theme {
--color-brand: #2563eb;
--color-brand-deep: #1e40af;
}
[data-theme='orange'] {
--color-brand: #f97316;
--color-brand-deep: #c2410c;
}
</style><html lang="zh-CN" data-theme="orange">普通 @theme 把令牌输出到 :root,[data-theme='orange'] 作用域用同名变量覆盖——所有 bg-brand/text-brand 在切换属性后自动取新值,工具类零改动(机制详见 050 篇第 3 节)。想加”页面内切换按钮”,一段 document.documentElement.dataset.theme = ... 即可。
任务三参考实现
<section data-slide
class="absolute inset-0 flex flex-col items-center justify-center gap-6
opacity-0 translate-y-8 pointer-events-none
transition-[opacity,translate] duration-500 ease-seat
data-[active]:opacity-100 data-[active]:translate-y-0 data-[active]:pointer-events-auto">
<h1 class="text-5xl font-black">
<span class="inline-block translate-y-4 opacity-0 transition-all duration-500 ease-seat
data-[active]_&:translate-y-0 data-[active]_&:opacity-100">网页</span>
<!-- 三个 span 同构,第二、三个再加 delay-100 / delay-200 -->
</h1>
</section>更稳的写法是把”后代响应祖先”收敛到父页一层:data-[active]:[&>span]:opacity-100 data-[active]:[&>span]:translate-y-0,三个 span 只带各自的 delay-*(delay-100、delay-200)。两种写法都要理解一件事——错峰的实质是过渡起点相同、延迟不同,靠 delay 制造波浪感,而不是给每个词写不同的动画。
9. 一句话记忆
单文件页面的三段式:直引 @tailwindcss/browser 换取零构建(仅限非生产)、@theme 收拢审美决策、data 属性 + 过渡工具类做状态切换(永远别用 display 硬切);装饰用同形 clip-path 叠色与低对比斜纹底纹;要演讲设施找 Reveal.js,要分发轻量就手写。
10. 相关阅读
- data 属性变体与叠加顺序:变体引擎与交互状态
- clip-path、斜纹渐变与毛玻璃:变换、滤镜与视觉特效
- 令牌体系与 data-theme 换肤:主题定制与设计令牌
- 过渡为何不能驱动 display:动画与过渡
参考与致谢
- 浏览器直引脚本与
<style type="text/tailwindcss">用法依据 Tailwind CSS 官方文档 v4(Play CDN 页,CC-BY 4.0):https://tailwindcss.com/docs/installation/play-cdn - Reveal.js 为开源软件(MIT License),其 fragment 机制见 https://github.com/hakimel/reveal.js