前置知识: HTML5、CSS

Tailwind 综合实战:从零搭一个单文件页面

10 min入门

以单文件 HTML 幻灯片为载体的 Tailwind 综合实战:浏览器直引与本地构建取舍、CSS 变量配色板接入 @theme、全屏 slide 容器与 data-active 切换动画、clip-path 几何切割与斜纹网格背景,并与 Reveal.js 方案对照

知识点地图

  • 知识类别:Tailwind 综合实战——把模块前序篇的工具类、变体、令牌、视觉特效,落成一个”双击就能打开”的单文件页面。
  • 解决什么问题:临时分享、课程演示、活动落地页这类”不需要工程化”的场景,如何用正确的姿势绕过构建步骤;以及幻灯片这种分屏切换页面应有的结构。
  • 什么时候用到:社团分享、快速原型、给非前端同事交付演示稿;也是检验前序篇掌握程度的期末测试。
  • 本篇是变体引擎与交互状态、变换、滤镜与视觉特效、主题定制与设计令牌的汇合点。

前置知识

学习目标

  • 能说出浏览器直引(@tailwindcss/browser)与本地构建各自的边界,并按场景选对。
  • 能在无构建的页面里用 <style type="text/tailwindcss"> 接入 @theme 配色板。
  • 能搭出”全屏 slide + data-active 切换 + cubic-bezier 缓动”的幻灯片骨架。
  • 能把 clip-path 几何切割与双色斜纹网格背景落进页面装饰。
  • 能说清手写方案与 Reveal.js 的取舍标准。

0. 从一份真实的社团幻灯片说起

一个社团招新宣讲,需要十来页幻灯片:讲师人手一份、现场电脑环境未知、还要发到群里让大家自己翻。约束倒推出技术选型——单个 HTML 文件,双击即开,不发依赖。这正是 Tailwind 的舒适区:所有样式都在类名里,浏览器直引后连 CSS 文件都不用发。

本篇按这份真实产物的思路从零走一遍:先定载入方式,再接配色板,然后搭全屏 slide 切换骨架,最后落两处几何装饰。走完你就得到了一个可复用的单文件页面模板。

1. 载入方式取舍:直引还是构建

Tailwind v4 提供一个官方浏览器内编译脚本,无需任何构建:

html
<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 会像构建模式一样处理它:

html
<style type="text/tailwindcss">
  @theme {
    --color-brand: #2563eb;
    --color-brand-deep: #1e40af;
    --color-accent: #f59e0b;
    --color-ink: #0f172a;
  }
</style>
html
<!-- 令牌立刻变成工具类 -->
<h1 class="text-brand">社团招新</h1>
<div class="bg-brand-deep text-white">深蓝底块</div>

讲解:与”散落写死 #2563eb”相比,令牌化之后全页只出现一次色值——临时换主题色只动一处(令牌原理的完整展开见主题定制与设计令牌)。单文件页面虽然小,令牌纪律不该松:色值、缓动曲线这类”审美决策”进 @theme,一次性装饰留给任意值。

3. 骨架:全屏 slide 与 data-active 切换

3.1 结构与状态约定

每页一个 <section>,绝对定位叠满全屏;切换状态不靠 display 硬切,而是”透明 + 位移”的组合动画——进入的页从下方浮上来,离开的页淡出:

html
<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

html
<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 几何切割色块

html
<!-- 封面页右下角的几何色块:斜切 + 双色叠加 -->
<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 叠加出网格:

html
<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 实现,逐条入场的分片动画是标配:

html
<!-- 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 节补全):

html
<!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 一类组合表达”后代响应祖先状态”。

先自己写,再对照参考实现:

任务一参考实现
html
<footer class="absolute bottom-4 right-6 text-sm text-gray-400"
        data-page-num>1 / 5</footer>
js
// show() 里追加一行:把当前页码写进每页的页码角标
const pagers = [...document.querySelectorAll('[data-page-num]')];
pagers.forEach((p) => (p.textContent = `${cur + 1} / ${slides.length}`));

链接不被劫持的关键在第 3.2 节的 closest('a, button')——事件先命中链接时直接放行,浏览器走默认导航。这个判断要放在”翻页”分支之前,顺序写反就成了”点链接先翻页”的事故。页码角标同步进 show() 而不是监听别的时机,保证状态只有一个变更入口。

任务二参考实现
html
<style type="text/tailwindcss">
  @theme {
    --color-brand: #2563eb;
    --color-brand-deep: #1e40af;
  }
  [data-theme='orange'] {
    --color-brand: #f97316;
    --color-brand-deep: #c2410c;
  }
</style>
html
<html lang="zh-CN" data-theme="orange">

普通 @theme 把令牌输出到 :root,[data-theme='orange'] 作用域用同名变量覆盖——所有 bg-brand/text-brand 在切换属性后自动取新值,工具类零改动(机制详见 050 篇第 3 节)。想加”页面内切换按钮”,一段 document.documentElement.dataset.theme = ... 即可。

任务三参考实现
html
<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. 相关阅读

参考与致谢