支持实时编码的 CSS Scroll Snap 幻灯片

Avatar of Stephanie Eckles
Stephanie Eckles

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 的免费积分!

虚拟会议改变了演讲者向观众传递内容的方式。在现场活动中,您可能只需使用笔记本电脑,但在家里,您可能拥有多个显示器,以便在进行实时编码演示时,您可以移动窗口并进行屏幕外更改。但是,随着一些活动重返线下,您可能和我一样想知道如何将同等体验带到现场会场。

通过使用原生 Web 功能和现代 CSS(如 CSS Scroll Snap)进行一些创造性思考,我们将构建一个无需 JavaScript 的幻灯片,它允许实时编辑 CSS 演示。由于最终的幻灯片存在于 CodePen 中,因此它将具有响应性和可共享性。

为了制作这个幻灯片,我们将学习以下内容:

  • CSS Scroll Snap、计数器和网格布局
  • contenteditable 属性
  • 使用自定义属性和 HSL 进行主题设定
  • 渐变文本
  • <style> 元素的样式

幻灯片模板

在制作包含许多不同幻灯片的幻灯片时,您可能需要不同类型的幻灯片。因此,我们将创建以下三种基本模板

  • 文本: 适用于您需要包含的任何文本
  • 标题: 突出显示标题以分隔内容部分
  • 演示: 带有代码块和预览的分割布局
Screenshot of the text slide containing a heading, byline, and Twitter handle. The text is dark blue and the background has a light blue. There is the number one in white inside a bright blue circle located in the bottom-left corner of the slide indicating the page title.
文本演示幻灯片
A slide with the text Topic 1 in dark blue and a soft linear gradient that goes from a super light blue to a brighter blue, going from left to right.
标题演示幻灯片

Slide deck screenshot showing the split view with live code on the left and the output on the right. The page number of the slide is shown in the bottom-left corner and includes the word CSS after the page number.
演示演示幻灯片

HTML 模板

让我们开始创建我们的 HTML。我们将使用一个 ID 为 slides 的有序列表,并填充文本和标题幻灯片。

每个幻灯片都是一个列表元素,具有 slide 类,以及一个修饰符类来指示模板类型。对于这些基于文本的幻灯片,我们嵌套了一个 <div>,该 <div> 具有 content 类,然后添加了一些样板文本。

<ol id="slides">
  <li class="slide slide--text">
    <div class="content">
      <h1>Presentation Title</h1>
      <p>Presented by Your Name</p>
      <p><a target="_blank" href="<https://twitter.com/5t3ph>">@5t3ph</a></p>
    </div>
  </li>
  <li class="slide slide--title">
    <div class="content">
      <h2>Topic 1</h2>
    </div>
  </li>
</ol>

由于 CodePen 使用 iframe 来预览,因此我们会在链接上使用 target="_blank",因此有必要“转义”iframe 并加载链接。

基本样式

接下来,我们将开始添加一些样式。如果您使用的是 CodePen,则这些样式假定您没有加载任何重置样式。我们的重置样式消除了边距,并确保 <body> 元素占据所有可用高度,这正是我们在这里需要的。此外,我们将进行基本的字体堆栈更新。

* {
  margin: 0;
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  font-family: system-ui, sans-serif;
  font-size: 1.5rem;
}

接下来,我们将定义所有主要布局元素都将使用CSS 网格,删除 #slides 的列表样式,并使每个幻灯片占据视窗大小。最后,我们将使用 place-content 简写来居中 slide--textslide--title 幻灯片内容。

body,
#slides,
.slide {
  display: grid;
}

#slides {
  list-style: none;
  padding: 0;
  margin: 0;
}

.slide {
  width: 100vw;
  height: 100vh;
}

.slide--text,
.slide--title {
  place-content: center;
}

然后,我们将添加一些轻量级的文本样式。由于这将是演示文稿,一次只有一个重点,而不是类似文章的格式,因此我们将基础 font-size 增加到 2rem。在全屏测试最终幻灯片时,请确保调整此值。您可能会发现它对于您的内容与演示文稿视窗大小相比太小了。

h1, h2 {
  line-height: 1.1;
}

a {
  color: inherit;
}

.content {
  padding: 2rem;
  font-size: 2rem;
  line-height: 1.5;
}

.content * + * {
  margin-top: 0.5em;
}

.slide--text .content {
  max-width: 40ch;
}

此时,我们有了一些大的文本,这些文本位于视窗大小容器的中心。让我们添加一些颜色,创建一个简单的主题系统。

我们将使用hsl 颜色空间 来设置主题,同时设置 --theme-hue--theme-saturation 的自定义属性。230 的色相值对应于蓝色。为了便于使用,我们将这些值组合到 --theme-hs 值中,以将其放入 hsl 实例中。

:root {
  --theme-hue: 230;
  --theme-saturation: 85%;
  --theme-hs: var(--theme-hue), var(--theme-saturation);
}

我们可以调整背景和文本的亮度值。幻灯片将感觉一致,因为它们将是该基本色调的色调。

回到我们的主要 <body> 样式中,我们可以应用这个想法来创建一个非常浅的版本颜色作为背景,以及一个深色版本作为文本。

body {
  /* ... existing styles */
  background-color: hsl(var(--theme-hs), 95%);
  color: hsl(var(--theme-hs), 25%);
}
Screenshot of a CSS scroll snap slide  with the presentation title, a byline, and a Twitter handle. The text is dark blue and the background is a light blue.

我们还可以为 .slide--title 添加一些额外的特色,方法是添加一个细微的渐变背景。

.slide--title {
  background-image: 
    linear-gradient(125deg, 
      hsl(var(--theme-hs), 95%), 
      hsl(var(--theme-hs), 75%)
    );
}
A slide with the text Topic 1 in dark blue and a soft linear gradient that goes from a super light blue to a brighter blue, going from left to right.

演示幻灯片模板

我们的演示幻灯片打破了目前的模式,需要两个主要元素:

  • 一个围绕内联 <style> 元素的 .style 容器,该元素包含您希望在屏幕上可见并应用于演示的实际书写样式
  • 一个 .demo 容器,用于保存演示预览,其中包含适合该演示的任何标记

如果您使用 CodePen 来创建此幻灯片,您需要将“行为”设置更新为关闭“保存时格式化”。这是因为我们不希望在样式块之前有额外的制表符/空格。确切的原因将在稍后说明。

Screenshot of a CodePen's HTMLand CSS code panels. The settings menu for the HTML panel is open and highlighting the first item, which is Format HTML.

这是我们的演示幻灯片内容:

<li class="slide slide--demo">
  <div class="style">
  <style contenteditable="true"> 
.modern-container {
  --container-width: 40ch;

  width: min(
    var(--container-width), 100% - 3rem
  );
  margin-inline: auto;
}
  </style>
  </div>
  <div class="demo">
    <div class="modern-container">
      <div class="box">container</div>
    </div>
  </div>
</li>

请注意 <style> 块上的额外 contenteditable="true" 属性。这是原生 HTML 功能,允许您将任何元素标记为可编辑 它不是表单输入和文本区域的替代品,通常需要 JavaScript 来实现更完整的功能。但对于我们的目的,它是实现“实时”编码的魔力。最终,我们能够对其中的内容进行更改,并且样式更改将立即应用。非常棒,请稍等。

但是,如果您查看目前的代码,您将看不到样式块的显示。您将看到 .modern-container 演示样式正在应用的结果,尽管如此。

这里另一个相关说明是 HTML5 包括验证在任何地方的 <style> 块;不仅仅是在 <head> 中。

我们接下来要做的操作可能感觉很奇怪,但我们实际上可以使用 <style> 上的 display 属性使其可见。我们将其放置在另一个容器中,以便为其使用一些额外的定位,并将其设为可调整大小的区域。然后,我们将 <style> 元素本身设置为 display: block,并应用属性以使其具有代码编辑器的外观和感觉。

.style {
  display: grid;
  align-items: center;
  background-color: hsl(var(--theme-hs), 5%);
  padding-inline: max(5vw, 2rem) 3rem;
  font-size: 1.35rem;
  overflow-y: hidden;
  resize: horizontal;
}

style {
  display: block;
  outline: none;
  font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
  color: hsl(var(--theme-hs), 85%);
  background: none;
  white-space: pre;
  line-height: 1.65;
  tab-size: 2;
  hyphens: none;
}

然后,我们需要创建 .slide--demo 规则,并使用 CSS 网格来并排显示样式和演示。作为提醒,我们已经设置了基本 .slide 类来使用网格,所以现在我们将为 grid-template-columns 创建一个仅适用于此模板的规则。

.slide--demo {
  grid-template-columns: fit-content(85ch) 1fr;
}

如果您不熟悉网格功能 fit-content(),它允许元素使用其内在宽度,直到达到函数中定义的最大值。因此,这条规则表示样式块可以扩展到最大 85ch 宽。当您的 <style> 内容较窄时,列的宽度仅为其所需的宽度。从视觉上看,这非常好,因为它不会创建额外的水平空间,同时最终限制了允许的宽度。

为了完善此模板,我们将为 .demo 添加一些内边距。您可能还注意到演示中 .box 的额外类。这是我喜欢用于演示的约定,以便在大小和位置很重要时,提供元素边界的视觉效果。

.demo {
  padding: 2rem;
}

.box {
  background-color: hsl(var(--theme-hs), 85%);
  border: 2px dashed;
  border-radius: .5em;
  padding: 1rem;
  font-size: 1.35rem;
  text-align: center;
}

这是我们的代码模板的结果:

Screenshot of a slide that's split in half vertically, the left side with a almost black dark blue background and code that is a lighter blue in a mono font. The right side has a light blue background and an element at the top that says container, with a dashed border and slightly darker blue background.

实时编辑功能

与显示的样式交互将实际更新预览!此外,由于我们创建了 .style 容器作为可调整大小的区域,因此您可以抓住右下角的调整大小句柄,根据需要放大和缩小预览区域。

实时编辑功能的一个警告是浏览器对它的处理方式不同。

  • Firefox: 这提供了最佳结果,因为它允许更改加载的样式,以及添加新属性甚至新规则的完整功能。
  • Chromium 和 Safari: 这些允许更改加载样式中的值,但不允许添加新属性或新规则。

作为演讲者,您可能希望使用 Firefox。对于使用演示文稿链接的观众来说,他们仍然可以获得幻灯片的意图,并且不应该在显示方面遇到问题(除非他们的浏览器不支持您演示的代码)。但是,在 Firefox 之外,他们可能无法像您在演示中展示的那样完全操作演示。

您可能希望“复制”完成的演示文稿 pen,并实际删除 <style> 块上的可编辑行为,而是显示最终版本的演示样式(如果适用)。

提醒:您在演示中包含的样式可能会影响幻灯片布局和其他演示!您可能希望将演示样式范围限定在特定于幻灯片的类下,以防止意外的样式更改跨越您的幻灯片。

代码高亮

虽然我们无法在没有 JavaScript 的情况下实现完整的语法高亮,但我们可以创建一个方法来突出显示代码块的某些部分以强调。

为此,我们将linear-gradient与允许使用元素背景作为文本效果的-webkit属性配对。然后,使用自定义属性,我们可以定义要突出显示的样式块的“行”数。

首先,我们将所需的-webkit属性直接放在<style>元素上。这将导致可见文本消失,但我们将通过添加背景使其在稍后可见。虽然这些是带有-webkit前缀的,但它们是跨浏览器支持的。

style {
  /* ...existing styles */
  -webkit-text-fill-color: transparent;
  -webkit-background-clip: text;
}

突出显示效果将通过创建一个具有两种颜色的linear-gradient来实现,其中较浅的颜色将作为突出显示行的文本颜色显示。默认情况下,我们将用较深的颜色包围突出显示,以便看起来第一个属性被突出显示。

以下是初始效果的预览

An up-close screenshot of the live code panel of the slide, with the second line of code a lighter blue than the rest, indicating that it is emphasized.

要创建此效果,我们需要找出如何计算突出显示颜色的高度。在我们的<style>元素规则中,我们已经将line-height设置为1.65,这对应于1.65em的总计算行高。所以,你可能会认为我们用行数乘以它就完事了。

但是,由于使用white-space: pre来保留换行符呈现可见的样式块,因此在第一行文本之前实际上有一个隐形的行。这是由在实际行上格式化<style>标签在第一行 CSS 代码之前创建的。这也是为什么我指出在 CodePen 中防止自动格式化很重要——否则,您还会有额外的左填充。

考虑到这些注意事项,我们将设置三个自定义属性来帮助计算我们需要的值并将它们添加到我们的.style规则集的开头。最后的--lines高度值首先考虑了那个不可见的行和选择器。

style {
  --line-height: 1.65em;
  --highlight-start: calc(2 * var(--line-height));
  --lines: calc(var(--highlight-start) + var(--num-lines, 1) * var(--line-height));
}

现在,我们可以应用这些值来创建linear-gradient。为了创建这种效果所需的急剧过渡,我们确保渐变从一种颜色到下一种颜色的停止匹配。

style {
  background-image: linear-gradient(
    hsl(var(--theme-hs), 75%) 0 var(--highlight-start),
    hsl(var(--theme-hs), 90%) var(--highlight-start) var(--lines),
    hsl(var(--theme-hs), 75%) var(--lines) 100%
  );
}

为了帮助可视化正在发生的事情,我已经注释掉了-webkit行以显示正在创建的渐变。

A close-up screenshot of the live code example, but with a bright blue background to reveal the near-white gradient that highlights the second line of code.

在我们的--lines计算中,我们还包含了一个--num-lines属性。这将允许您通过内联样式调整每个演示要突出显示的行数。此示例将突出显示调整为三行

<style contenteditable="true" style="--num-lines: 3">

我们也可以传递一个重新计算的--highlight-start来更改突出显示的初始行

<style contenteditable="true" style="--num-lines: 3; --highlight-start: calc(4 * var(--line-height))">

让我们看看之前调整的结果

Showing the live code example with lines 3 through 6 highlighted in a lighter blue than the rest of the code.

现在,如果您在演示期间添加或删除行,突出显示将不会调整。但它仍然是帮助引导观众注意力的工具。

我们将添加两个实用程序类,用于仅突出显示规则或完全删除突出显示。要使用,请直接应用于演示的<style>元素。

.highlight--rule-only {
  --highlight-start: calc(1 * var(--line-height))
}

.highlight--none {
  background-image: none;
  background-color: currentColor;
}

使用 CSS 滚动捕捉的幻灯片动画

好的,我们有一些看起来不错的初始幻灯片。但它还不完全像幻灯片演示。我们将分两部分解决这个问题

  1. 水平重新排列幻灯片
  2. 使用CSS 滚动捕捉来强制一次滚动一个幻灯片

我们的初始样式已经将#slides有序列表定义为网格容器。为了实现水平布局,我们需要添加一个额外的属性,因为.slides已经包含了填充视窗的尺寸。

#slides {
  /* ...existing styles */
  grid-auto-flow: column;
}

为了使 CSS 滚动捕捉起作用,我们需要定义允许溢出的轴,因此对于水平滚动,那是x

#slides {
  overflow-x: auto;
}

我们为#slides容器需要的最后一个滚动捕捉属性是定义scroll-snap-type。这是一个简写,我们选择x轴,以及mandatory行为,这意味着启动滚动应该始终触发捕捉到下一个元素。

#slides {
  scroll-snap-type: x mandatory;
}

如果您现在尝试,您将不会体验到滚动捕捉行为,因为我们还有两个属性需要添加到子.slide元素中。使用scroll-snap-align告诉浏览器“捕捉”到哪里,并将scroll-snap-stop设置为always可以防止滚动超过子元素。

.slide {
  /* ...existing styles */
  scroll-snap-align: center;
  scroll-snap-stop: always;
}

滚动捕捉行为应该通过滚动幻灯片或使用左右箭头键来工作。

还有更多可以为 CSS 滚动捕捉设置的属性,您可以查看 MDN 文档以了解所有可用的属性。CSS 滚动捕捉在跨浏览器和跨输入类型(如触摸与鼠标,或触摸板与鼠标滚轮,或通过滚动条箭头)方面也有略微不同的行为。对于我们的演示,如果您发现滚动不是很平滑或“捕捉”,那么尝试使用箭头键。

目前,还没有办法自定义 CSS 滚动捕捉滑动动画的缓动或速度。也许这对你的演示很重要,而且你不需要我们为修改代码示例开发的其他功能。在这种情况下,你可能想选择一个“真正的”演示应用程序。

CSS 滚动捕捉非常酷,但如果你想在我们的幻灯片演示之外使用它,也要注意一些注意事项。查看SmolCSS.dev 上的另一个滚动捕捉演示和更多信息。

幻灯片编号

一个可选的功能是添加可见的幻灯片编号。使用 CSS 计数器,我们可以获取当前幻灯片编号,并将其作为伪元素的值以我们想要的方式显示。使用数据属性,我们甚至可以附加当前主题。

第一步是给我们的计数器命名,这可以通过counter-reset属性来完成。它被放在包含要计数的项目的元素上,所以我们将把它添加到#slides中。

#slides {
  counter-reset: slides;
}

然后,在要计数的元素(.slide)上,我们添加counter-increment属性并回调到我们定义的计数器的名称。

.slide {
  counter-increment: slides;
}

要访问当前计数,我们将设置一个伪元素。在content属性中,counter()函数可用。此函数接受我们计数器的名称并返回当前编号。

.slide::before {
  content: counter(slides);
}

该数字现在出现了,但不在我们想要的地方。因为我们的幻灯片内容是可变的,所以我们将使用经典的绝对定位将幻灯片编号放置在左下角。我们将添加一些视觉样式,使其包含在一个漂亮的小圆圈中。

.slide::before {
  content: counter(slides);
  position: absolute;
  left: 1rem;
  bottom: 1rem;
  width: 1.65em;
  height: 1.65em;
  display: grid;
  place-content: center;
  border-radius: 50%;
  font-size: 1.25rem;
  color: hsl(var(--theme-hs), 95%);
  background-color: hsl(var(--theme-hs), 55%);
}
Screenshot of the text slide containing a heading, byline, and Twitter handle. The text is dark blue and the background has a light blue. There is the number one in white inside a bright blue circle located in the bottom-left corner of the slide indicating the page title.

我们可以通过获取数据属性的值来增强我们的幻灯片编号,以附加一个简短的主题标题。这意味着首先将属性添加到每个<li>元素,我们希望这样做。我们将data-topic添加到<li>中以获取标题和代码演示幻灯片。该值可以是你想要的任何值,但较短的字符串将显示得最好。

<li class="slide slide--title" data-topic="CSS">

我们将使用该属性作为选择器来更改伪元素。我们可以使用attr()函数获取该值,我们将它与幻灯片编号连接起来,并添加一个冒号作为分隔符。由于该元素之前是一个圆圈,因此还需要更新一些其他属性。

[data-topic]::before {
  content: counter(slides) ": " attr(data-topic);
  padding: 0.25em 0.4em;
  width: auto;
  border-radius: 0.5rem;
}

添加了这些内容后,以下是代码演示幻灯片,显示添加的主题“CSS”

Slide deck screenshot showing the split view with live code on the left and the output on the right. The page number of the slide is shown in the bottom-left corner and includes the word CSS after the page number.

小型视窗样式

我们的幻灯片已经有一些响应性,但最终,在较小的视窗上会出现水平滚动的問題。我的建议是删除 CSS 滚动捕捉,并让幻灯片垂直流动。

要实现这一点,只需要进行少量更新,包括添加边框以帮助分隔幻灯片内容。

首先,我们将#slides的 CSS 滚动捕捉相关属性移动到媒体查询中,以仅在超过120ch时应用。

@media screen and (min-width: 120ch) {
  #slides {
    grid-auto-flow: column;
    overflow-x: auto; 
    scroll-snap-type: x mandatory;
  }
}

接下来,我们将.slide的 CSS 滚动捕捉和尺寸属性也移动到此媒体查询中。

@media screen and (min-width: 120ch) {
  .slide {
    width: 100vw;
    height: 100vh;
    scroll-snap-align: center;
    scroll-snap-stop: always;
  }
}

要堆叠演示内容,我们将.slide--demo的整个规则移动到此媒体查询中

@media screen and (min-width: 120ch) {
  .slide--demo {
    grid-template-columns: fit-content(85ch) 1fr;
  }
}

现在一切都堆叠在一起,但我们想要恢复每个幻灯片的最小高度,然后添加我之前提到的边框

@media (max-width: 120ch) {
  .slide {
    min-height: 80vh;
  }

  .slide + .slide {
    border-top: 1px dashed;
  }
}

您的内容也可能在较小的视窗上发生溢出,因此我们将对.content进行一些调整,以尝试防止这种情况。我们将添加一个将在小型视窗上使用的默认宽度,并将之前的max-width约束移动到媒体查询中。此外还展示了一种快速方法,更新我们的<h1>以使用流体类型。

h1 {
  font-size: clamp(2rem, 8vw + 1rem, 3.25rem);
}

.content {
  /* remove max-width rule from here */
  width: calc(100vw - 2rem);
}

@media screen and (min-width: 120ch) {
  .content {
    width: unset;
    max-width: 45ch;
  }
}

此外,我发现重新定位幻灯片计数器很有帮助。为此,我们将调整初始样式,将其放置在左上角,然后将其移回我们的媒体查询中的底部。

.slide {
  /* adjust default here, removing the old "bottom" value */
  top: 0.25rem;
  left: 0.25rem;
}

@media (min-width: 120ch) {
  .slide::before {
    top: auto;
    bottom: 1rem;
    left: 1rem;
  }
}

最终幻灯片

嵌入的内容可能会显示堆叠的小视窗版本,因此请确保在 CodePen 中打开完整版本,或跳转到实时视图。提醒一下,编辑功能在 Firefox 中效果最佳。

如果您有兴趣查看实际应用的完整演示文稿,我使用此技术展示了我的现代 CSS 工具包.