Slidev 单页作用域样式完全指南:用<style>让每一页 CSS 彼此隔离,并善用 UnoCSS 嵌套与全局样式
【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev
在本仓库(Slidev,Presentation Slides for Developers)中,每张幻灯片本质上都是一个独立的 Vue 组件,因此在 Markdown 中直接书写<style>块,即可定义只作用于当前这一页的 CSS,而不会泄漏或污染其他幻灯片。本文以 skills/slidev/references/style-scoped.md 及其官方文档 docs/features/slide-scope-style.md 为骨架,结合仓库源码讲解:scoped属性是如何被自动注入的、作用域化后选择器会受到哪些限制、如何借助 UnoCSS 指令与嵌套 CSS 简化写法,以及何时应当改用全局样式文件。读完你可以在自己的演示项目中熟练、安全地为单页定制样式。
基础用法:仅作用于当前幻灯片的<style>
在任意一页幻灯片的 Markdown 内容中,直接写入<style>标签即可为该页定义样式。Slidev 的 Markdown 渲染管线和 Vue 组件化机制会确保它只影响当前这一页:
# This is Red <style> h1 { color: red; } </style> --- # Other slides are not affected效果说明:
- 上例中,第一页(包含
<style>的页面)的h1标题会变成红色; - 用
---分隔的第二页同样含有h1,但不会继承红色样式,因为它位于不同的幻灯片组件中。
这与 Vue 单文件组件(SFC)中<style scoped>的语义一致:每页幻灯片会被编译成一个独立的 Vue 组件,样式只挂载在属于该组件的内容上。你可以在 packages/slidev/node/vite/vue.ts 看到 Vite 的 Vue 插件将.md与.md?vue一并纳入编译范围,这正是"每页内容 = 组件 + scoped 样式"能成立的基础。
默认全作用域:自动注入scoped的源码原理
文档强调:<style>标签在幻灯片中默认就是作用域化的(Scoped by Default),你不需要也不应该手动写<style scoped>(虽然写了也会被保留)。
这一行为由 Slidev 内置的 markdown-it 插件实现,源码位于 packages/slidev/node/syntax/scoped.ts:
const RE_STYLE_TAG_OPEN = /<style\b([^>]*)>/gi const RE_SCOPED_ATTR = /\bscoped\b/i export default function MarkdownItStyleScoped(md: MarkdownExit) { const addScoped = (html: string) => { return html.replace(RE_STYLE_TAG_OPEN, (match, attrs) => { if (RE_SCOPED_ATTR.test(attrs)) return match return `<style scoped${attrs}>` }) } // 同时改写 html_block 与 html_inline 两条渲染规则 }从源码可以看到三点实现细节:
- 插件用正则
<style\b([^>]*)>匹配渲染产物的每一个<style>标签,若标签属性中没有scoped,就把它改写为<style scoped ...>; - 如果你显式写了
<style scoped>,正则检测到已有scoped属性后原样保留,不会重复添加; - 该插件在 packages/slidev/node/syntax/index.ts 的
useMarkdownItPlugins()中被无条件注册(md.use(MarkdownItStyleScoped)),也就是说这是 Slidev 对所有演示的默认行为,不需要任何配置开关。
配套的单测 packages/slidev/node/syntax/scoped.test.ts 验证了三种关键场景:
// 1. 自动添加 scoped // <style> → <style scoped> it('add scoped to style', ...) // 2. 已有 scoped 的原样保留 // <style scoped> → <style scoped> it('preserve existing scoped', ...) // 3. 代码块内的 <style> 不会被改写(被当作源码展示) // ```html\n<style>... → <pre><code><style>... it('not transform in code block', ...)第三个测试用例非常实用:如果你用代码围栏(fenced code block)展示"怎么写<style>",围栏内容会被转义成普通文本而不会被执行,幻灯片样式也不会受影响。因此你完全可以在某一页演示源码、在另一页真实书写<style>而互不干扰。
注意:自动注入的
scoped属性负责标记"这段样式属于哪个组件",真正让选择器带上作用域标记(例如 Vue 编译后形如[data-v-xxxxx])的是 Vue SFC 编译器对<style scoped>的处理。Slidev 的插件与 Vue 编译两级配合,才实现"默认全作用域"。
作用域带来的限制:为何子组合器.a > .b不可用
既然样式是全作用域的,就必然要遵守 Vue Scoped CSS 的规则。文档明确指出:子组合器(Child Combinator),例如.a > .b,因作用域机制而无法按预期工作。
原理简述(与 Vue SFC 的 scoped CSS 一致):作用域化编译时,选择器会被改写并在末位元素上附加data-v-xxx这类标记,例如直接子元素选择器可能被改写成.a > .b[data-v-xxx]的形式,而中间/父级选择器并不保证带标记,导致"直接子元素"的匹配语义被破坏。所以这类"必须命中 DOM 父子结构"的选择器,无法像在普通全局 CSS 中那样直接套用。
如果你确实需要这类匹配,实践中通常有两条路:
- 重构选择策略:改为对当前组件内元素作用更大的类名,例如把要修饰的元素包在带 class 的容器里,再写后代选择器
.wrap .b,或者干脆直接给目标元素加一个类; - 将样式移到全局:真正依赖 DOM 结构全局生效的规则(例如命中布局组件内部结构),应当放入全局样式文件(见下文"何时使用全局样式"一节)。
UnoCSS 加持:嵌套 CSS 与--uno指令
Slidev 以 UnoCSS 作为内置的原子化 CSS 引擎。因此在<style>块里,除了书写普通 CSS,你还能直接使用嵌套 CSS与UnoCSS 指令(--uno:/@apply等),官方文档给出的示例:
# Slidev > Hello **world** <style> blockquote { strong { --uno: 'text-teal-500 dark:text-teal-400'; } } </style>这段代码做的事情可以拆解为三层:
blockquote { strong { ... } }是嵌套 CSS写法,等价于blockquote strong { ... },它只对当前页引用块里的<strong>生效;- 内层的
--uno: 'text-teal-500 dark:text-teal-400'是 UnoCSS 的指令语法,相当于内联声明一组工具类:text-teal-500以及带dark:变体的dark:text-teal-400(深色主题下换用text-teal-400),因此无需记住具体 CSS 值; - 整个规则仍处于作用域内,不会污染其他页面的引用块。
能在<style>中使用这些语法,是因为 Slidev 客户端的 UnoCSS 配置显式开启了指令转换器。参见 packages/client/uno.config.ts:
transformers: [ transformerDirectives({ enforce: 'pre' }), transformerVariantGroup(), ],其中transformerDirectives负责处理@apply、--uno:(即--at-apply的别名)等指令。基于同一机制,你也可以用更常见的@apply语法书写:
<style> blockquote { strong { @apply text-teal-500 dark:text-teal-400; } } </style>(在@apply与--uno:中选择其一即可,两者表达的意图相同。)正是因为有这条enforce: 'pre'的转换链,Slidev 内置在 packages/client/uno.config.ts 中还注册了forward:/backward:之类的方向变体,配合深色模式变体,你可以在单页作用域内写出非常精细的条件样式。
全局样式:何时使用styles/index.css,如何被加载
作用域样式适合"本页专属",但有些需求天然是全局的,例如:
- 统一定义配色变量、字体基线或作用于所有页面的基础排版;
- 命中由布局组件(Layout)渲染的结构,而非当前页 Markdown 直接生成的内容。
官方文档的指引是:需要全局样式时,请使用项目根目录下的styles/index.css(详见 docs/custom/directory-structure.md 的 Style 一节)。Slidev 遵循如下目录约定并自动注入到应用根部:
Pattern: ./style.css | ./styles/index.{css,js,ts}也就是说,项目根目录下放置style.css或styles/index.css(也支持styles/index.ts/styles/index.js,便于用import './a.css'手动管理多个样式文件的引入顺序)即可被自动加载。背后的实现位于 packages/slidev/node/virtual/conditional-styles.ts:该虚拟模块会遍历项目的每个入口根目录,用 import glob 匹配以下候选路径并注入 import:
join(root, 'styles/index.{ts,js,css}'), join(root, 'styles.{ts,js,css}'), join(root, 'style.{ts,js,css}'),如果你需要引入多个 CSS 入口,推荐styles/目录 + 一个入口文件统一管理的结构,例如:
/* styles/index.css */ @import './base.css'; @import './code.css';同时注意一个关键区别:
- 放在
styles/index.css(全局)里的规则会命中任何页面,适合上述全局性需求; - 写在单页 Markdown
<style>里的规则只命中当前页,适合做局部微调且不用担心与其他页面冲突。
实践要点小结
围绕"单页作用域样式"这一主题,把最容易被忽略的规则汇总如下,方便你在写演示时对照使用:
| 场景 | 做法 | 依据 / 参考 |
|---|---|---|
| 只想美化当前页 | 在该页 Markdown 中直接写<style>,无需手写scoped | scoped.ts 自动注入scoped |
已有<style scoped> | 无需改动,编译会原样保留 | scoped.test.ts |
在文档里展示<style>示例代码 | 放进代码围栏(如 ```html),不会被当作真实样式执行 | 同上,测试用例 3 |
需要.a > .b这类子组合器 | 作用域下不可靠,请重构选择器或改用全局样式 | Vue Scoped CSS 语义 |
| 想用原子化工具类写局部样式 | 在<style>中使用嵌套 CSS +--uno:或@apply | packages/client/uno.config.ts 的transformerDirectives |
| 需要全局性样式/配色变量 | 使用根目录styles/index.css(或styles/index.{js,ts}/style.css) | conditional-styles.ts 自动注入 |
| 深入阅读 | 官方专题文档 | slide-scope-style.md、config-unocss.md |
最后再强调一次核心结论:在 Slidev 中,Markdown 内的<style>永远是页面级作用域的——这是由 packages/slidev/node/syntax/scoped.ts 在渲染阶段统一改写、再由 Vue 编译层施加作用域标记共同保证的默认行为。理解这一点后,你就可以放心大胆地用"每页一个<style>"的方式做局部精修,并把主题级、跨页级的样式交给styles/index.css,让整套演示的样式边界清晰、易于维护。
【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考