这是一本关于我在为 CSS-Tricks 网站工作期间遇到的我最喜欢的技巧的汇编。其中大部分技巧不是我自己的,而是来自比我聪明得多的人。在这里,我将展示它们并解释我所理解的技巧。虽然其中一些技巧可能比其他技巧更“有用”,但从所有技巧中都能学到很多东西,无论你是否直接使用这些技巧。

本书中没有明确介绍的一个技巧是“CSS 绘图”的技巧。这些年来,我见到了成千上万这样的绘图,尤其是在我观看人们在 CodePen 上玩耍和学习时,CodePen 是我帮助运营的另一个网站。这些绘图从用 `border-radius: 50%`、背景颜色和绝对定位制作笑脸的圆圈,到用渐变、阴影和所有 CSS 技术制作的诚实到神的骗眼杰作。为什么?人们不可避免地会问。为什么用 CSS 来做这件事,而你可以用 SVG?或者使用专门用于制作艺术品的绘图软件?为什么不呢?可能是一个更好的问题。艺术不需要被束缚在某人可能认为更高效的东西的界限之内。但更重要的是,讽刺的是,我发现那些做这种 CSS 绘图和探索的人,无论多么“不切实际”,最终也会成为在“实际”问题上更出色的 CSS 实践者。

所以,朋友们,享受这些 CSS 技巧吧!我希望它们能给你带来一些快乐,即使你不能立即在你的工作中使用它们,对它们的了解也会让你成为一个更狡猾的 CSS 开发人员,也许你甚至没有意识到这一点。

第 1 章 扭曲视觉

扭曲视觉(Squigglevision)是一个(真实的!)动画术语,指线条看起来在扭动,即使物体/场景处于静止状态。这是像 Dr. Katz 这样的节目标志性的外观的一部分,还记得吗?

非常独特的外观!它甚至获得了专利。但是专利中谈到的是五张经过编辑的图像,并将它们以“快速连续”的方式显示。维基百科

为了创建构成扭曲视觉的线条振荡效果,Tom Snyder Productions 的动画师在称为“flic”的序列中循环播放五张略微不同的图画。

在网络上,如果我们要快速连续地动画化五张(或更多)图像,我们可能会使用基于 `step()` 的 `@keyframes` 动画和一个精灵图来实现。这是一个由 simuari 提供的很好的例子,它展示了它的工作原理,精灵图在上面(10 张图像组合成 1 张),动画在下面。

但这太费事了!我们有一种方法可以使任何元素产生抖动、摇晃和扭曲的效果,而无需手工制作一堆单独的图像并制作专门尺寸的定制关键帧来实现它。

技巧是什么?

快速迭代的 SVG 湍流滤镜

什么?是的,太酷了。

我从 David Khourshid 那里学到了这个技巧,他制作了一个很棒的演示,Alex the CSS Husky(见下文),扭曲效果甚至不是该演示的主要功能!David 说他从 Lucas Bebber 的另一个演示中获得了这个技巧,我将在下面嵌入。

(这是我为了解决 Firefox 的一个小问题而分叉的版本:你不能在 Firefox 中使用 `display: none;` 隐藏 SVG。)

以下是单个 SVG 湍流滤镜的工作原理。首先,你用一些内联的 `<svg>` 来声明它

<svg display="none">
  <defs>
    <filter id="turb">
      <feTurbulence baseFrequency="0.3" numOctaves="2" />
      <feDisplacementMap in="SourceGraphic" scale="20" />
    </filter>
  </defs>
</svg>

然后你可以将它应用到任何 HTML 元素,就像这样

.filter {
  filter: url("#turb");
}

这里有一个前后对比

这是一个相当极端的湍流。试着把它调低到 `baseFrequency="0.003"`,看看更细微的版本。嗯,看起来有点像非常轻微的扭曲,不是吗?

技巧是只使用一点点,制作几个不同的滤镜,然后在它们之间进行动画。

这里有五个不同的湍流滤镜,每个滤镜都略微不同,并且具有不同的 ID

<svg>
  <filter id="turbulence-1">
    <feTurbulence type="fractalNoise" baseFrequency="0.001" numOctaves="2" data-filterId="3" />
    <feDisplacementMap xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" scale="25" />
  </filter>

  <filter id="turbulence-2">
    <feTurbulence type="fractalNoise" baseFrequency="0.0015" numOctaves="2" data-filterId="3" />
    <feDisplacementMap xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" scale="25" />
  </filter>

  <filter id="turbulence-3">
    <feTurbulence type="fractalNoise" baseFrequency="0.002" numOctaves="2" data-filterId="3" />
    <feDisplacementMap xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" scale="25" />
  </filter>

  <filter id="turbulence-4">
    <feTurbulence type="fractalNoise" baseFrequency="0.0025" numOctaves="2" data-filterId="3" />
    <feDisplacementMap xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" scale="25" />
  </filter>

  <filter id="turbulence-5">
    <feTurbulence type="fractalNoise" baseFrequency="0.003" numOctaves="2" data-filterId="3" />
    <feDisplacementMap xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" scale="25" />
  </filter>

</svg>

以及一个 CSS 关键帧动画,可以在它们之间进行动画

@keyframes squigglevision {
  0% {
    filter: url("#turbulence-1");
  }
  25% {
    filter: url("#turbulence-2");
  }
  50% {
    filter: url("#turbulence-3");
  }
  75% {
    filter: url("#turbulence-4");
  }
  100% {
    filter: url("#turbulence-5");
  }
}

你可以将其应用于任何你想让它扭曲的东西

.squiggle {
  animation: squigglevision 0.4s infinite alternate;
}

这几乎与 Alex the CSS Husky 中发生的事情一模一样,只是滤镜更平滑。

这是 Lucas 的原始演示

第 2 章 硬停止渐变

这是一个“传统”渐变的例子,其中颜色从一种颜色缓慢地渐变到另一种颜色。

这些都是用 CSS 渐变创建的

渐变中有一个叫做颜色停止的概念,它允许你控制颜色从一种颜色过渡到下一种颜色的位置。这里有一个例子,其中第一种颜色在大部分区域都保持不变

技巧是:颜色停止可以彼此越来越靠近,并且实际上可以位于同一个点。这意味着颜色不会过渡,而是停留在一个颜色,然后另一个颜色可以从一个精确的点开始。这是一个关于收敛颜色停止的可视化解释

下面的示例看起来几乎像是两个具有不同背景的单独元素,但实际上,它是一个具有硬停止渐变的单个元素,它在视觉上将空间分割开来。如果你需要制作垂直列并在一个父元素上处理它们的背景,这是一种可能性!事实上,由于背景将覆盖整个区域,因此你无需担心元素“拉伸”到完整的高度,这使得它在我们需要制作基于浮动或内联块元素的列时成为一个很棒的技巧。

扩展硬停止的概念,我们可以制作一个彩色条纹。以下是由移动 `background-position` 制作的变体。

说到条纹,这些硬停止渐变非常适合任何类型的条纹背景。使用重复渐变(例如 `repeating-linear-gradient()`)会更容易一些,因为你无需填充 100% 的空间,可以使用像素并在需要的地方停止。

还有其他类型的渐变!我们也可以在 `radial-gradient` 和 `repeating-linear-gradient` 中使用硬停止!

请注意,在最后一个示例中,你仍然看到一些颜色渐变的东西。硬停止渐变并不一定只能单独使用。它只有一个重复的硬停止。

圆锥形渐变是另一个硬停止渐变的最佳候选,因为它应用到圆形(例如 `border-radius: 50%`)中时,会立即变成饼图!

第 3 章 可拖动元素

为了清楚起见,即使我们在 HTML 和 CSS 中实现了这一点,我们所做的只是使元素在屏幕上可拖动。如果你实际上需要执行一些操作来响应拖动,你就回到了 JavaScript 的领域。

这个技巧来自Scott Kellum。多年来,Scott 做了很多我最喜欢的 CSS 技巧,比如这个超级简单的 @keyframes 设置,它使元素像老式屏幕保护程序一样从视窗边缘弹回,以及一个令人印象深刻的由 Sass 驱动的视差技术

实际上只有一个 CSS 东西可以帮助我们进行点击拖动,那就是我们在使用 `resize` 属性时在桌面浏览器上获得的浏览器 UI。这里有一个 `<div>`,我们在其中使用它(以及 `overflow: hidden;`,这是它工作的前提条件)

如果你在桌面浏览器中查看演示,你将能够抓住它右下角并将其拖动。

现在真正的技巧来了。

我们可以将该可调整大小的元素放入另一个容器中。该容器会随着可调整大小的元素的高度变化而自然地增长高度。它会由于 `width: min-content;` 而自然地改变宽度。

现在我们有一个父元素,它会随着可调整大小的元素一起调整大小。这很重要,因为我们可以在该父元素中放置其他东西,这些东西会随着它一起移动。我将在此处放置一个大大的 ✖,并将它直接放在调整大小器上面,并使用 `pointer-events: none;`,这样我仍然可以进行调整大小

现在,如果我们通过 `opacity: 0;` 确保调整大小元素被隐藏,看起来我们像是凭空创建了一个可拖动元素!我们可能需要调整一下数字才能使东西对齐,但这是可以做到的

第 4 章 可编辑样式块

当你看到一些像这样的 HTML 时

<p>I'm going to display this text.</p>

这是相当直观的。比如,浏览器将显示该段落元素的文本。

但该段落存在于一个更大的 HTML 文档中,比如

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Website</title>
</head>
<body>
  <p>I'm going to display this text.</p>
</body>
</html>

为什么“我的网站”没有像段落那样显示?`<title>` 和 `<p>` 有什么不同?嗯,这就是任何代码的本质。不同的东西做不同的事情。但在这种情况下,我们可以很容易地追溯它为什么没有显示

如果我们在浏览器中打开这个 HTML 文档并检查该 `<title>` 元素,它会向我们显示用户代理样式表将该元素设置为 `display: none;` 嗯,这有道理!这正是我们在希望完全隐藏网站上的东西时使用的方法。

更重要的是,`<title>` 元素的父元素 `<head>` 也是 `display: none;`。

这就是有趣的地方。

用户代理样式很容易覆盖!来自我们 CSS 的任何值都会覆盖它。所以让我们试试这个

<style>
  head, title {
    display: block;
  }
</style>

LOLZ,它出现了!就像一个段落

我们对它的控制就像对其他任何东西一样,这意味着我们可以对其应用完美的样式

这会变得更加奇怪……看到 `<head>` 中的 `<style>` 块了吗?我们看不到它,原因与我们看不到 `<title>` 一样,因为它也是 `display: none;`。我们也可以更改它使其可见。

趁着这个机会,我们可以让它看起来像我们在代码编辑器中看到的一样,让它尊重空白并使用等宽字体

head, title, style {
  display: block;
}
style {
  font-family: monospace;
  white-space: pre;
}

哈!这到底是怎么回事!

现在我们可以变得更加奇怪。那个样式块?它可以变得可编辑,因为这在任何 HTML 元素中都可以使用 `contenteditable` 属性来实现。

<style contenteditable>
  ...
</style>

现在,该可见的 `<style>` 块可以像 `<textarea>` 一样进行编辑,并且 CSS 会立即应用于文档。

这绝对是 HTML 和 CSS 可以做到的最奇怪的事情之一。

你可能会称之为 CSS 昆廷(“一个自引用程序,无需任何外部访问即可输出其自身源代码。”)。Alex Sexton 在 2013 年发布了一个示例,并认为 Anne van Kesteren 和 Mathias Bynens 为先例。我看到 Lea Verou 在会议演讲中用它来进行现场编码!

第 5 章 滚动阴影

滚动阴影的概念非常有意义。当容器向下滚动时,您可以在顶部看到一个阴影,这清楚地表明您可以向上滚动。如果可以向下滚动,则那里也会有阴影,除非您已滚动到底部。

这可能只是我最喜欢的 CSS 技巧。这个想法来自 Roman Komarov,但 Lea Verou 想出了更精妙的 CSS 技巧,并 将其推广

滚动阴影是一种非常好的 UX,几乎让人怀疑为什么它不是原生浏览器功能,或者至少在 CSS 中更容易实现。你可以称它们为“提示”,一个明显的视觉提示,表明滚动是可能的或已完成,不需要任何学习。

以下是一个工作示例

理解它的工作原理有点令人费解,部分原因是它使用了background-attachment: local;,这是一种很少使用的技术。以下是一个尝试

  1. 这里有两种类型的阴影在起作用
    1. 常规阴影
    2. 覆盖 阴影
  2. 所有阴影都是用背景渐变创建的。例如,一个非重复的radial-gradient,大小和位置位于元素的中心顶部,看起来像一个阴影。
  3. 覆盖阴影通过多个背景的堆叠顺序放置在这些常规阴影的顶部,并且能够完全隐藏它们。
  4. 常规阴影使用background-attachment的默认值,即scroll,您应该对此很熟悉,因为它是背景在元素滚动时通常的工作方式,您不会真正去思考它。背景只是存在,位于元素的可见部分,并且不会随着元素的滚动而移动。
  5. 溢出阴影使用不常见的background-attachment: local;,将其放置在元素的顶部和底部边缘,考虑元素的整个滚动高度。它们会随着元素滚动位置的移动而移动。

因此,想象一下这种情况:元素垂直溢出,并且当前已滚动到最顶部。顶部阴影和顶部阴影覆盖都位于元素的顶部。覆盖位于顶部,隐藏了阴影,就好像它根本不存在一样。向下滚动一点,覆盖会粘贴到元素的最顶部,现在被溢出隐藏,因此您再也看不到覆盖,阴影就会显露出来。在底部,您可以一直看到阴影,因为覆盖会粘贴到元素的最底部,而阴影会粘贴到可见区域的底部。滚动到底部,覆盖将与底部阴影重叠,将其隐藏。这听起来很复杂,但它确实有效!

它的美妙之处在于它只需要几行代码,就可以应用于单个元素以完成它。

.scroll-shadows {
  max-height: 200px;
  overflow: auto;

  background:
    /* Shadow Cover TOP */
    linear-gradient(
      white 30%,
      rgba(255, 255, 255, 0)
    ) center top,
    
    /* Shadow Cover BOTTOM */
    linear-gradient(
      rgba(255, 255, 255, 0), 
      white 70%
    ) center bottom,
    
    /* Shadow TOP */
    radial-gradient(
      farthest-side at 50% 0,
      rgba(0, 0, 0, 0.2),
      rgba(0, 0, 0, 0)
    ) center top,
    
    /* Shadow BOTTOM */
    radial-gradient(
      farthest-side at 50% 100%,
      rgba(0, 0, 0, 0.2),
      rgba(0, 0, 0, 0)
    ) center bottom;
  
  background-repeat: no-repeat;
  background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
  background-attachment: local, local, scroll, scroll;
}

它不必只适用于白色背景,但它需要是一个纯色。

以下是一个将颜色抽象到 CSS 自定义属性的版本

这不仅仅是一个 UI/UX 的细节,它对于在容器没有滚动条或任何其他 UI 来指示它可以滚动的情况下,指示容器是否有更多可以滚动的内容至关重要。考虑一下 Tyler Hall 的故事 Perfectly Cropped,其中他分享了家人对 iOS 13 中的分享面板感到困惑。完全不明显的是您可以在此处向下滚动 在此屏幕截图中

其他技巧

也许阴影可以根据可滚动内容的多少而变得更大或更强?

Hakim El Hattab 曾经在推特上发布了一个示例,很好地展示了这一点。

请注意,该演示使用了一些 JavaScript 来实现其功能。当然,我被纯 CSS 版本吸引,尤其是这里,因为它很容易应用于单个元素。但还有很多使用 JavaScript 的方法,例如

尽管我被只在 CSS 中完成这项工作所吸引,但还有另一个原因可能让你想要使用 JavaScript 驱动的解决方案:iOS Safari。早在 2019 年 6 月,当 iOS 13 发布时,该版本(以及此后的所有版本)中的 Safari 无法使用此技术。事实上,我不确定为什么。它看起来像个 bug。它在巧妙的 纯 CSS 驱动的视差在 iOS Safari 上崩溃 的同时崩溃了。它可能与它们如何“缓存”网站的某些绘制层有关。如果他们能修复它就好了。

第 6 章 完美的字体回退

当你在网络上加载自定义字体时,一个负责任的做法是确保@font-face声明使用属性font-display设置为swapoptional之类的值。

@font-face {
  font-family: 'MyWebFont'; /* Define the custom font name */
  src:  url('myfont.woff2') format('woff2'); /* Define where the font can be downloaded */
  font-display: swap; /* Define how the browser behaves during download */
}

有了它,用户加载你的页面之前不会有任何延迟才能看到文本。这对性能来说很棒。但它也带来了设计上的权衡,用户将看到 FOUT 或“未设置文本的闪烁”。这意味着他们将看到页面加载时使用一种字体,然后字体加载后,页面将切换到另一种字体,从而导致视觉干扰,并且可能发生一些重新布局。

这个技巧就是为了最小化这种干扰和重新布局!

这个技巧来自 Glen Maddern,他在 Front End Center 上发布了一个关于此的屏幕录像,他使用了 Monica Dinculescu 的 Font style matcher 与 Bram Stein 的 Font Face Observer 库相结合。

假设你从 Google Fonts 加载一个字体。在这里,我将使用 Rubik 的两种粗细。

@import url("https://fonts.googleapis.com/css2?family=Rubik:wght@400;900&display=swap");

在该 URL 的末尾,默认情况下,你会看到&display=swap,这是它们确保font-display: swap;位于@font-face声明中的方式。

在缓慢的连接中,这是包含文本的简单页面加载方式

首先,您将看到回退排版,然后自定义字体将加载,您将看到排版切换为使用这些字体。

看到切换了吗?记住,从某种意义上说,这很好,因为至少文本一开始是可见的。但切换过于强硬。感觉上视觉上很干扰。

让我们修复它。

使用 Font style matcher 工具,我们可以将这两种字体叠加在一起,看看 Rubik 和回退字体有多不同。

请注意,我在这里使用system-ui作为回退字体。您需要使用经典的“网络安全”字体作为回退,例如 Georgia、Times New Roman、Arial、Tahoma、Verdana 等。大多数计算机默认安装了这些字体,因此它们是安全的回退。

在我们的例子中,这两种字体的“x 高度”几乎完全相同(请注意红色和黑色小写字母的高度)。如果它们不同,我们将不得不调整字体大小和行高以匹配。但幸运的是,对字母间距进行微调就能使它们非常接近。

将回退调整为使用letter-spacing: 0.55px;,使它们的大小非常接近!

现在,诀窍是在字体加载之前应用此样式。因此,让我们将其设置为默认样式,然后使用一个主体类来告诉我们字体已加载,并删除更改

body {
  font-family: "Rubik", system-ui, sans-serif;
  letter-spacing: 0.55px;
}
body.font-loaded {
  letter-spacing: 0px;
}

但是如何获得该font-loaded类呢?Font Face Observer 库 使得它变得非常容易,并且跨浏览器兼容。在该库就位后,只需几行 JavaScript 就可以调整该类

const font = new FontFaceObserver("Rubik", {
  weight: 400
});

font.load().then(function() {
  document.body.classList.add("font-loaded");
});

现在看看字体加载体验有多么平滑和不那么干扰

那真是一个非常棒的技巧!

以下是一个简单的演示

在测试时,如果您根本看不到切换,请检查以确保您的机器上没有安装 Rubik 字体。或者你的网络速度可能太快了!DevTools 可以帮助您降低连接速度以进行测试

随着您使用多种字体和多种字体粗细,这会变得更加复杂。您可以监控每种字体的加载情况,并在它们加载时调整不同的类,调整样式以确保重新布局尽可能少。

第 7 章 自绘制形状

我们在网络上拥有的最佳形状绘制工具是 SVG,尤其是<path d="" />元素。使用 路径语法,您可以使用其绘制直线和曲线线的命令来绘制任何您想要的东西。路径可以是一个实心形状,但在这里,为了我们的目的,让我们假设路径是fill: none;,我们将专注于路径的stroke,并使该路径自行绘制。

假设我们有一个单独的<path />,它绘制了一个像这样的酷炫形状

在我们的 SVG 中,我们将确保以这种方式很好地设置路径

<path 
  pathLength="1" 
  stroke="black"
  stroke-width="5"
  fill="none"
  d="..."
/>

第二行将使这个技巧非常容易实现,正如你一会儿就会看到的那样。

技巧本身,让形状“自我绘制”,来自这样的想法:笔划可以是*虚线的*,并且你可以*控制虚线的长度和偏移量*。所以**想象一下:**你让虚线(和虚线后的空格)*很长*,以至于它覆盖了整个形状,所以它看起来好像没有虚线笔划。但是,然后你再次偏移笔划,以至于看起来根本没有笔划。**然后这里是关键:**动画化偏移量,以使它看起来像是形状正在自我绘制。

这就是为什么pathLength="1" 非常有用。我们只是在动画化从10 的偏移量,这在 CSS 中很容易

path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: dash 5s linear forwards;
}

@keyframes dash {
  from {
    stroke-dashoffset: 1;
  }
  to {
    stroke-dashoffset: 0;
  }
}

上面的 CSS 将在*任何*带笔划的路径上起作用,假设你正在使用pathLength 技巧!

一个小问题:Safari。Safari 不喜欢路径上的pathLength 属性,因此简单的 1 到 0 技巧会失效。不过它可以弥补。首先,我们需要计算出路径的自然长度(而不是强迫它为 1)。我们可以通过在 DOM 中选择它来做到这一点,然后

path.getTotalLength();

在上面的示例中,长度为 8085。因此,我们将在 CSS 中使用该值而不是 1。

path {
  stroke-dasharray: 8085;
  stroke-dashoffset: 8085;

  animation: dash 5s ease-in-out infinite alternate;
}

@keyframes dash {
  from {
    stroke-dashoffset: 8085;
  }
  to {
    stroke-dashoffset: 0;
  }
}

这里有一个示例的分支,其中包含该值,它将在所有浏览器中都能正常工作。希望 Safari 能让pathLength 起作用,因为它比测量路径长度要容易得多。

更多

第 8 章 方块按钮

我们将讨论这些“方块按钮”,但最终我们将使用box-shadow 来制作它们,所以让我们快速了解一下box-shadow 的旅程。

box-shadow 的基本用途是通过在元素下方应用阴影,使元素看起来具有三维效果,就好像它从表面上抬起来一样。

这些白色方块上应用的轻微阴影由以下代码完成

.module {
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

也就是说

  1. 制作元素形状的精确副本(例如,尊重border-radius),并将其放在元素下方
  2. 将其水平偏移 0,垂直偏移 1px(向下)
  3. 将其模糊 3px。在模糊之后有一个可选参数称为扩展,它允许你扩展或收缩阴影,默认值为 0(既不扩展也不收缩)。
  4. 它的背景将是黑色,不透明度为 0.2

不过,这太基本了。来吧。我们可以做得更怪异。考虑一下

  1. 你可以对这些偏移量进行极端操作。
  2. 你不需要模糊阴影。
  3. 颜色不需要是微妙的。

最重要的是

  1. 你可以应用多个阴影

这里有三个偏移不同的阴影,没有模糊

.module {
  width: 100px;
  height: 100px;
  background: white;
  box-shadow:
    5px 5px 0 #FF9800,
    10px 10px 0 #FFC107,
    15px 15px 0 #607D8B;
}

我们可以进一步推迟这些偏移量,使“阴影”完全与元素分离

.module {
  width: 50px;
  height: 50px;
  background: white;
  box-shadow:
    55px 55px 0 #FF9800,
    110px 110px 0 #FFC107,
    165px 165px 0 #607D8B;
}

所以现在我们知道我们可以拥有任意大小的无限阴影,它们可以放置在任何位置……我们可以用一个元素绘制像素艺术!全部用一个元素!这里有一个由 Marcus Connor 完成的汉堡包、薯条和奶昔

史蒂夫·乔布斯,由 Codrin Pavel 完成

或者,如何用大约 7500 个阴影完成的蒙娜丽莎,由 Jay Salvat 完成

稍微更实用的层面上,你可以叠加box-shadow 来模拟三维效果和方向阴影。**方块按钮!**

技巧是我们使用零模糊阴影,将它们叠加在一起。如果我们一次做一个像素,并在操作时交替使用两侧,那么阴影叠加在一起的方式将使我们有机会创建一个 3D 方块的外观。以下是一些基本知识

.boxy-button {
  --bottom-color: #999;
  --right-color: #ddd;

  box-shadow:
      1px 0   0 var(--right-color),
      1px 1px 0 var(--bottom-color),
      2px 1px 0 var(--right-color),
      2px 2px 0 var(--bottom-color),
      3px 2px 0 var(--right-color),
      3px 3px 0 var(--bottom-color),
      4px 3px 0 var(--right-color),
      4px 4px 0 var(--bottom-color);
}

继续这样操作,我们就可以制作一个非常方块的按钮。

添加一些过渡效果,我们甚至可以让它感觉非常可按下

我们也可以对“外部”阴影使用逐行阴影技术,通过每次稍微降低阴影的不透明度来模拟渐变。这使得它更像是一种方向阴影,看起来很酷。

这里有一个示例,其中方向相反(由于负的 box-shadow 偏移量),并使用方向阴影。

对于一个有趣的按钮来说,这真是很多代码,但按钮不值得吗?用更少的代码,我们可以获得另一个非常有趣的偏移外观,只是这次使用了一些inset box-shadow 技巧和小伪元素来伪造连续的边框外观。

第 9 章 滚动指示器

有一个内置的浏览器功能来指示你的滚动位置。注意:它是滚动条,它做得很好。这些天甚至有一种标准化的方式来设置滚动条的样式。

body {
  --scrollbarBG: #CFD8DC;
  --thumbBG: #90A4AE;

  scrollbar-width: thin;
  scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}

你可能想将这些与-webkit- 样式组合,以获得最佳的浏览器支持。例如

但假设你对设置滚动条的样式并不感兴趣,而是要构建自己的指示器来显示用户滚动了多远。就像一个进度条,当你接近阅读文章的结尾时它就会填满。

Mike Riethmuller 找到了一种极其聪明的方法来做到这一点!

它不仅聪明,而且是用极少的代码完成的。为了理解,让我们删除标题上的白色背景和主体上的伪元素,以显示所使用的linear-gradient

啊哈!一个带有硬停止的斜向渐变。我们已经可以看到这里发生了什么。当页面向下滚动时,你可以看到这个渐变的那一部分变得越来越蓝。然后,技巧就变成了隐藏除了这个渐变的一小条之外的所有东西,因此在标题和伪元素上使用了实色背景,它们相隔几个像素。

也许最巧妙的部分是渐变背景的大小。你可能认为它只是覆盖了整个背景,但事实并非如此。如果你这样做,滚动条将永远不会完成,因为它位于页面的顶部,而渐变在页面的底部完成。由于此演示位于页面中部,因此渐变需要在底部完成一个几乎完整的视窗高度。这将看起来像

background-size: 100% calc(100% - 100vh);

除了固定标题大小会影响这一点,因此需要减去它。最后,代码看起来好像包含了很多神奇数字。但它们并不完全是神奇的,因为它们中的大多数在基因上是相关的。这里有一个分支,将它们全部转换为自定义属性,这样你就可以看到这一点。

为什么要这样做呢?

如果你想做一些非常花哨的事情,比如显示你滚动了页面多少的百分比,或者更花哨的事情,比如显示一个按程序计算的估计阅读时间,那么,这些都是可行的,但你需要使用 JavaScript

第 10 章 边框三角形

想象一个带有粗边框的元素

.triangle {
  width: 200px;
  height: 200px;
  border: 10px solid black;
}

现在想象所有四个边框都有不同的颜色

.triangle {
  ...

  border-left-color:    red;
  border-right-color:   yellowgreen;
  border-top-color:     orange;
  border-bottom-color:  rebeccapurple;
}

注意边框是如何以角度相交的吗?

看看当我们将元素的宽度和高度折叠为零时会发生什么

.triangle {
  ...

  width: 0;
  height: 0;
}

如果其中三个边框是transparent,那么我们就会得到一个三角形!

.triangle {
  ...

  border-left-color: transparent;
  border-right-color: transparent;
  border-top-color: transparent;
  border-bottom-color: rebeccapurple;
}

很好。

这对于像指向文本的气泡这样的东西可能很有用。在这种情况下,你可以通过伪元素将三角形添加到另一个元素中。这里有一个完整示例

第 11 章 灵活网格

CSS 网格 有学习曲线,就像其他任何东西一样,但过了一会儿,它就会变得很清晰。你设置一个网格(实际上是列和行),然后将东西放在这些行上。我认为它的思维模式比弹性盒子 简单一些。

这里,我将设置一个网格

.grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 1rem;
}

现在,如果我有三个子元素要放在这个网格上……

<div class="grid">
   <div></div>
   <div></div>
   <div></div>
</div>

它们将完美地落在网格上。这非常容易,并且为我们提供了很多控制。1fr 单位可以根据需要调整。如果第一个是2fr 而不是1fr,那么它将占用另外两个元素的两倍空间。如果它是200px,那么它将正好是这么宽。gap 可以加宽和缩窄。有各种工具可以用于对齐、显式放置和排序。

让我们考虑一下其他事情。假设只有两个子元素。那么,如果我们没有显式说明要将它们放在哪里,它们将自动落在第 1 列和第 2 列。假设有 5 个子元素。那么,第 4 个和第 5 个将自动向下移动到新的一行。是的,!到目前为止,我们的网格完全忽略了行,它们只是隐含的。这很直观。你不需要关心行,它们可以自动创建。你可以明确说明它们,但你不需要这样做。

以下是 CSS 技巧:我们可以将这种“不需要关心” 的乐趣扩展到以及行。

一种方法是通过……

  1. 不设置任何grid-template-columns
  2. 将自动流从默认的行更改为grid-auto-flow: column;

现在将有与子元素数量一样多的列!此外,你仍然可以使用gap,这很好。

但我们在这里失去换行。如果列数是根据可以在不破坏父元素宽度的情况下容纳多少个元素来确定的,然后对网格的其余部分也这样做,那就太好了。

这将我们引向了也许是整个 CSS 网格中最有名和最实用的代码

.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

还有一个auto-fill 关键字,它们略有不同,正如Sara Soueidan 解释的那样

如果你调整此示例的大小,你将看到列数是如何调整的

还要注意,这里使用的最小值为每列 200px。这只是一个你认为对你的内容来说感觉不错的数字。如果这个数字是,比如,400px,那么你可能会考虑进行一个修改,如果屏幕本身小于这个宽度,那么就允许它变小。我第一次从 Evan Minto 那里看到这个技巧

grid-template-columns: repeat(auto-fill, minmax(min(10rem, 100%), 1fr));

这句话的意思是,如果 100% 的宽度计算结果小于 10rem(否则为最小值),那么就使用它,使其更适合小屏幕布局。

第 12 章 形状变形

网络上有很多运动的可能性。你可以很容易地动画化任何元素的opacitycolortransform 属性(比如translatescalerotate),仅举几例。例如

.kitchen-sink {
  opacity: 0.5;
  background-color: orange;
  transform: translateX(-100px) scale(1.2) rotate(1deg);
}
.kitchen-sink:hover {
  opacity: 1;
  background-color: black;
  transform: translateX(0) scale(0) rotate(0);
}

顺便说一下,对transformopacity属性进行动画处理是理想的,因为正如他们所说,浏览器可以“廉价地”执行此操作。这意味着浏览器在执行移动操作时需要做的工作要少得多,并且可以利用“硬件加速”。

鲜为人知的是,你可以对元素的实际形状进行动画处理!我说的不仅仅是对border-radius进行动画处理或移动一些伪元素(尽管这当然很有用),我的意思是真正地对元素的矢量形状进行变形。

为了我们的第一个技巧,让我们通过clip-path来创建矢量形状。我们可以像这样在元素的百分比坐标上剪切掉部分元素

.moving-arrow {
  width: 200px;
  height: 200px;
  background: red;
  clip-path: polygon(100% 0%, 75% 50%, 100% 100%, 25% 100%, 0% 50%, 25% 0%);
}

Clippy是一个生成polygon()形状数据的强大工具。Firefox DevTools也内置了相当不错的工具,可以用来在应用后对它进行操作。

然后我们可以在某种状态变化时改变clip-path。它可以是类的改变,但让我们在这里使用:hover。顺便说一下,让我们添加一个过渡,以便我们可以看到形状的变化!

.moving-arrow {
  ...
  transition: clip-path 0.2s;
  clip-path: polygon(100% 0%, 75% 50%, 100% 100%, 25% 100%, 0% 50%, 25% 0%);
}
.moving-arrow:hover {
  clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%);
}

由于polygon()具有完全相同的坐标数量,因此过渡效果有效。

使用clip-path来绘制矢量形状很好,但它可能不是绘制矢量形状的最佳工具。绘制矢量形状实际上是SVG的领域。SVG的元素具有专门用于绘制的属性。强大的元素是具有自身特殊语法的元素,用于绘制。

你可能会看到这样的路径

<svg viewBox="0 0 20 20">
  <path d="
     M 8 0
     L 12 0
     L 12 8
     L 20 8
     L 20 12
     L 12 12
     L 12 20
     L 8 20
     L 8 12
     L 0 12
     L 0 8
     L 8 8
     L 8 0
  "></path>
</svg>

它绘制了一个“+”形状。

d属性的值可能看起来像乱码,但它实际上只是在指挥一支虚拟笔的移动。将笔移动到这里,在这个方向上绘制一条长度为这么长的线,等等。

在上面的示例中,只使用了两个命令,你可以从每个行之前的字母中看出

如果我们想要,SVG本身具有一种语言可以用来修改这些坐标,包括动画。 它被称为SMIL,但它最大的问题是它已经过时,而且从来没有得到过很好的支持。

好消息是,一些浏览器直接在CSS中支持SMIL可以做到的部分功能。例如,我们可以像这样在CSS中更改:hover上的path

svg:hover path {
    d: path("
      M 10 0 
      L 10 0
      L 13 7
      L 20 10
      L 20 10
      L 13 13
      L 10 20
      L 10 20
      L 7 13
      L 0 10
      L 0 10
      L 7 7
      L 10 0
    ");
}
path {
  transition: 0.2s;
}

这将我们的加号形状变成了一个飞镖形状,并且过渡效果是可能的,因为它具有相同数量的点。

如果你真的热衷于变形形状,并且想要一个非常强大的工具来帮助你实现它,请查看Greensock的MorphSVG插件。它允许你对形状变形的控制非常多,并且不受相同数量点的过渡限制。

第 13 章 黄闪

我们将会讨论黄闪,但这与页面滚动位置以及对当前位置的理解有关。如今,CSS本身可以对页面上的滚动位置进行动画处理。这是一行代码!

html {
  scroll-behavior: smooth;
}

在某种程度上,这是一个审美选择。当页面从一个位置滚动到另一个位置(例如,由于一个指向页面上另一个ID的“跳转链接”)时,页面会平滑地滚动到该位置,而不是立即跳到那里。除了美观之外,使用它的目的是帮助用户了解上下文。哦,我明白了,页面从这里滚动到这里,它停止的位置就是我链接到的位置。

但平滑滚动并不是唯一一种强调在同一页面上链接到其他位置的上下文的方式。我们还可以对目标区域进行视觉更改,我认为这样更清晰明了。

幸运的是,有一个CSS伪选择器非常适合在对页面内链接的元素进行样式化时使用。:target选择器,它可以用于任何元素

section:target {
  background: yellow;
}

这意味着:当URL中的#hash与该元素的#id匹配时,此选择器将匹配并像这样对其进行样式化。

所以,如果存在像这样的元素...

<section id="footnotes">

</section>

而URL恰好是

https://website.com/#footnotes

该选择器将匹配,并且我们将会看到它有一个黄色的背景。

当点击像这样的链接时,就会出现该URL

<a href="/#footnotes">Jump to Footnotes</a>

当然,这里有一个技巧。仅仅一个黄色的背景(或任何其他静态样式)并不能真正突出显示你刚刚链接到这里!一点动画在这里可以起到很大的作用。如果你不是简单地将链接到的元素的背景设置为黄色,而是暂时闪烁一个黄色的背景,那么它就足以吸引眼球,并将操作变得非常清晰。

这里有一个示例和演示。假设你有一个指向文章底部脚注的链接。这尤其有趣,因为指向页面底部的链接特别难以引起注意(可能没有足够的滚动空间将脚注滚动到页面的顶部)。

<p>
  Lorem ipsum<sup class="footnote" id="footnote-top-1">
    <a href="#footnote-bottom-1">1</a></sup>
  dolor sit amet consectetur adipisicing elit.
</p>

然后在页面的底部,你链接到的实际脚注

<div id="footnotes" class="footnotes">
  <ol>
    <li id="footnote-bottom-1">Lorem ipsum is Greek. 
      <a href="#footnote-top-1">
        <span class="screen-reader-text">Back to reference</span>
        ↥
      </a></li>
  </ol>
</div>

请注意,这两个锚链接都使用跳转链接,footnote-bottom-1footnote-top-1分别指向这些ID。

我们可以使用@keyframes动画使脚注本身在你到达时闪烁

.footnotes :target {
  animation: yellowflash-bg 2s;
}
@keyframes yellowflash-bg {
  from { background: yellow; }
  to   { background: transparent; }
}

在这种情况下,它会立即闪烁为黄色,然后在2秒内逐渐淡化为透明的背景。

这是一个交互式示例

这就是黄闪!当然,它不一定要是黄色的,甚至不一定要闪烁。关键是要做一些事情来直观地表明为了清晰起见所链接的内容。

上面的演示与平滑滚动相结合,但你可能想这样做,因为你无法控制平滑滚动的时机,因此有可能当你到达那里时黄闪已经结束了。

嘿,制作一个摇晃也可能很有趣。

第 14 章 将滚动固定到底部

CSS 中的overflow-anchor属性 相对来说比较新,它于 2017 年首次在 Chrome¹、2019 年首次在 Firefox 中推出,现在 Edge 也在与 Chrome 的过渡中采用了它2020 年。幸运的是,它的使用在很大程度上是一种增强。其理念是浏览器真的试图允许位置发生改变,这是默认的行为。然后,如果你不喜欢它处理这种情况的方式,你就可以使用overflow-anchor来关闭它。因此,通常情况下,你永远不会触碰它。

但正如你可能猜到的那样,我们可以利用这种小巧的美妙功能来做一些CSS技巧。我们可以强制一个可滚动元素始终固定在底部,即使我们在其中添加了新的内容。

聊天是固定到底部滚动的典型例子。

我们期望在像 Slack 这样的 UI 中出现这种行为,如果我们已经滚动到某个频道中最近的消息的底部,当有新消息到达时,它们会立即出现在底部,我们不需要手动重新滚动到底部才能看到它们。

这个功能来自Ryan Hunt,他也提到了 Nicolas Chevobbe。

正如 Ryan 所说

你是否曾经尝试实现一个可滚动元素,其中正在添加新内容,并且你想要将用户固定到底部?正确地实现这一点并不容易。

至少,你需要使用 JavaScript 检测何时添加了新内容,并强制可滚动元素滚动到底部。以下是一种使用 JavaScript 中的MutationObserver 来监视新内容并强制滚动的技巧

const scrollingElement = document.getElementById("scroller");

const config = { childList: true };

const callback = function (mutationsList, observer) {
  for (let mutation of mutationsList) {
    if (mutation.type === "childList") {
      window.scrollTo(0, document.body.scrollHeight);
    }
  }
};

const observer = new MutationObserver(callback);
observer.observe(scrollingElement, config);

这里有一个演示

但我发现只用 CSS 的解决方案更具吸引力!上面的版本有一些我们将在后面讨论的 UX 缺陷。

这里的技巧是,浏览器默认情况下已经做了滚动锚定。 但是浏览器试图做的事情是让你页面发生移动。因此,当添加了可能改变页面视觉位置的新内容时,它们试图阻止这种情况发生。在这种不寻常的情况下,我们想要的是相反的效果。我们希望页面固定在页面的底部,并且让视觉页面在视觉上移动,因为它被强制固定到底部。

以下是技巧的工作原理。首先,在可滚动父元素中添加一些 HTML

<div id="scroller">
  <!-- new content dynamically inserted here -->
  <div id="anchor"></div>
</div>

所有元素都自然地拥有overflow-anchor: auto;,这意味着它们试图阻止页面在屏幕上时发生移动,但我们可以使用overflow-anchor: none;来关闭它。因此,诀窍是定位所有动态插入的内容,并将它关闭

#scroller * {
  overflow-anchor: none;
}

然后强制只有该锚元素具有滚动锚定,其他元素都没有

#anchor {
  overflow-anchor: auto;
  height: 1px;
}

现在,一旦该锚点在页面上可见,浏览器将被强制将该滚动位置固定到它,由于它是该可滚动元素中最后的东西,因此将始终固定到底部。

我们开始吧!

这里有两个小问题...

  1. 注意,锚点必须具有一定的大小。我们在这里使用height来确保它不是一个折叠的/空的元素,没有大小,这将阻止它工作。
  2. 为了使滚动锚定正常工作,页面必须至少滚动一次。

第二个问题比较难。一种选择是根本不去处理它,你可以等到用户滚动到底部,然后从那时起效果就会生效。实际上,这还不错,因为如果他们从底部滚动离开,效果就会停止工作,这正是你想要的。在上面的 JavaScript 版本中,请注意它如何强制你滚动到底部,即使你尝试向上滚动,这就是 Ryan 所说的实现起来并不容易的原因。

如果你需要立即启动效果,诀窍是让可滚动元素立即变得可滚动,例如

body {
  height: 100.001vh;
}

然后立即触发一个非常小的滚动

document.scrollingElement.scroll(0, 1);

这应该可以解决问题。这些行可以在上面的演示中取消注释,你可以尝试一下。

  1. 说到 Chrome,谷歌非常重视这种布局移动问题。Web Core Vitals 之一是累积布局偏移(CLS),它衡量视觉稳定性。如果你的 CLS 分数很低,它会直接影响你的 SEO。

第 15 章 滚动动画

所以让我们先把这个弄清楚。使用 JavaScript 的一行代码,我们可以设置一个 CSS 自定义属性,它知道页面滚动百分比

window.addEventListener('scroll', () => {
  document.body.style.setProperty('--scroll', window.pageYOffset / (document.body.offsetHeight - window.innerHeight));
}, false);

现在我们有了--scroll值,可以用于 CSS 中。

这个技巧来自Scott Kellum,他是 CSS 技巧大师!

让我们先设置一个使用该值的动画。这是一个简单的 SVG 元素旋转动画,它会一直旋转

svg {
  display: inline-block;
  animation: rotate 1s linear infinite;
}

@keyframes rotate {
  to {
    transform: rotate(360deg);
  }
}

这里就是技巧所在!现在让我们暂停这个动画。我们不是在一段时间内对它进行动画处理,而是通过调整页面的滚动位置来对它进行动画处理,通过调整animation-delay来实现。如果animation-duration是1s,这意味着滚动整个页面长度,也就是动画的一个迭代。

svg {
  position: fixed; /* make sure it stays put so we can see it! */

  animation: rotate 1s linear infinite;
  animation-play-state: paused;
  animation-delay: calc(var(--scroll) * -1s);
}

尝试将animation-duration更改为0.5s。这样,当页面使用animation-delay数学方法向下滚动时,可以完成动画的两个完整循环。

Scott 在他的原始演示中指出,设置...

animation-iteration-count: 1;
animation-fill-mode: both;

... 也会解释一些“过度”的奇怪现象,我也可以证明我也见过,尤其是在较短的视窗上,所以设置这些也很值得。

Scott 还将与滚动相关的动画属性设置在 :root {} 本身,这意味着它可以同时控制页面上的所有动画。 这里是他演示,它同时控制了三个动画。


本期内容就到这里了,各位!我非常希望能够继续制作新的内容,而且我当然也已经瞄准了一些技巧。但是,如果你认为有一些技巧值得在这里封存,请 告诉我