使自定义属性(CSS 变量)更加动态

Avatar of Dan Wilson
Dan Wilson

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

CSS 自定义属性(可能更容易理解为 CSS 变量)为我们提供了使代码更简洁的方法,并引入了以前不可能的 CSS 工作方式。 它们可以做到预处理器变量可以做到的……但更多。 无论您是 CSS 声明式性质的粉丝还是更喜欢在 JavaScript 中处理大部分样式逻辑,自定义属性都为每个人提供了价值。

大部分力量来自于自定义属性的两个独特功能

  • 级联
  • 使用 JavaScript 修改值的能力

当您将自定义属性与其他现有的 CSS 概念(如 calc())结合使用时,将展现出更多力量。

基础知识

您可以使用自定义属性来有效地执行 Sass 等预处理器中的变量所提供的内容——设置全局或作用域变量值,然后在代码中使用它。 但由于级联,您可以在更具体的规则内赋予新的属性值。

正如 Violet Peña 在她的 变量关键益处概述 和 Chris 在他的 网站主题选项汇总 中所示,这种级联可以带来几种有趣的方法。

人们已经讨论了这些级联带来的益处几年了,但它经常在对话中被遗忘,尽管它是将自定义属性与预处理器区分开来的关键功能。 2014 年,Amelia Bellamy-Royds 在 SVG 和 use 的背景下 讨论了它,Philip Walton 在 2015 年指出了很多 这些一般的级联益处,而去年 Gregor Adams 展示了它们如何在 一个最小的网格框架 中使用。 利用级联可能是开始使用自定义属性并牢记渐进增强最简单的方式。

好的。 现在我们知道自定义属性可以让我们原生实现一些预处理器提供的功能,并且由于级联还可以实现一些新的用途——它们是否提供了我们以前从未能够实现的功能?

当然!

个性化属性

所有具有多个部分的属性现在都可以以不同的方式使用。 多个 background 可以分开,多个 transition-duration 可以单独拆分。 而不是使用像 transform: translateX(10vmin) rotate(90deg) scale(.8) translateY(5vmin) 这样的规则,您可以设置一个包含多个自定义属性的规则,然后在之后独立更改这些值。

.view { 
  transform: 
    translateX(var(--tx, 0))
    rotate(var(--deg, 0))
    scale(var(--scale, 1))
    translateY(var(--ty, 0));
}
.view.activated {
  --tx: 10vmin;
  --deg: 90deg;
}
.view.minimize {
  --scale: .8;
}
.view.priority {
  --ty: 10vmin;
}

初始化需要一些时间,但之后前期的一点点额外努力就可以让您根据类/选择器规则的需求独立修改每个转换函数。 然后,您的标记可以在每个 .view 元素上包含任何或所有定义的类,并且 transform 将相应更新。

虽然独立的转换属性即将到来(届时 translatescalerotate 将成为一级公民),但它们目前仅在 Chrome 中以标志的形式提供。 使用自定义属性,您可以使用更多支持(以及定义自己的函数顺序的能力,例如 rotate(90deg) translateX(10vmin)translateX(10vmin) rotate(90deg) 不同)来获得此功能。

如果您不介意它们共享相同的计时选项,当使用 transition 更改任何变量时,它们甚至可以平滑地动画。 真是神奇。

查看 Dan Wilson(@danwilson)在 CodePen 上创建的 CSS 变量 + 转换 = 个性化属性(带输入)

从无单位到所有单位

您可以将这些概念与 calc() 结合使用来构建。 与始终像上面那样使用单位设置变量(--card-width: 10vmin--rotation-amount: 1turn)不同,您可以去掉单位,并在更多地方使用它们,并且它们之间具有关联性。 现在,我们自定义属性中的值可以比以前更动态。

虽然 calc() 已经存在几年了,但当尝试从使用不同单位的值的加法中获得结果时,它可以说是最有用。 例如,您有一个以百分比单位表示的流体 width,需要将其缩短 50px(width: calc(100% - 50px))。 但是,calc() 具有更多功能。

calc 内允许其他运算(如乘法)来调整值。 以下操作有效,并让我们了解转换和过滤器彼此相关,因为它们都使用数字 10。

.colorful {
  transform: 
    translateX(calc(10 * 1vw))
    translateY(calc(10 * 1vh));
  filter: hue-rotate(calc(10 * 4.5deg));
}

这可能不是一个常见的用例,因为它是一个不需要浏览器计算的计算。 10 * 1vw 将始终为 10vw,因此 calc 对我们没有帮助。 当使用具有循环的预处理器时,它可能有用,但这是一个较小的用例,通常可以在不需要 CSS calc() 的情况下完成。

但是,如果我们将那个重复的 10 替换为变量呢? 您可以根据单个值在多个地方设置值,即使使用不同的单位也是如此,并且可以打开在未来更改值的选项。 以下操作由于无单位变量和 calc 而有效

.colorful {
  --translation: 10;
  transform: 
    translateX(calc(var(--translation) * 1vw))
    translateY(calc(var(--translation) * 1vh));
  filter: hue-rotate(calc(var(--translation) * 4.5deg));

  will-change: transform, filter;
  transition: transform 5000ms ease-in-out, filter 5000ms linear;
}

.colorful.go {
  --translation: 80;
}

查看 Dan Wilson(@danwilson)在 CodePen 上创建的 单个自定义属性,多个计算

可以获取单个值(最初为 10,或稍后更改为 80……或任何其他数字),并将其分别应用于 vw 单位或 vh 单位以进行转换。 您可以将其转换为 deg 以进行旋转或 filter: hue-rotate()

您不必在变量上删除单位,但只要您在 calc 中有它们,就可以删除它们,并且它可以打开在其他地方以更多方式使用它的选项。 可以通过在不同的规则中修改基本值来完成动画编排以偏移持续时间和延迟。 在此示例中,我们始终希望 ms 作为我们的最终单位,但我们想要的主要结果是我们的 delay 始终是动画 duration 的一半。 然后,我们可以通过仅修改我们的 --duration-base 来做到这一点。

查看 Dan Wilson(@danwilson)在 CodePen 上创建的 基于持续时间的延迟

甚至三次贝塞尔曲线也可以进行自定义属性修改。 在以下示例中,有几个堆叠的框。 每个框的比例略小,并且每个框都提供了一个三次贝塞尔曲线乘数。 此乘数将分别应用于基线三次贝塞尔曲线的四个部分。 这使每个框都可以拥有一个不同的三次贝塞尔曲线,但与其他框之间存在关联。 尝试删除或添加框以查看它们如何彼此交互。 按任意位置将框转换为该点。

查看 Dan Wilson(@danwilson)在 CodePen 上创建的 螺旋轨迹……有点

JavaScript 用于在每次按下时随机化基线,以及设置每个框的乘数。 然而,CSS 的关键部分是

.x {
  transform: translateX(calc(var(--x) * 1px));
  /* baseline value, updated via JS on press */
  transition-timing-function: 
    cubic-bezier(
      var(--cubic1-1),
      var(--cubic1-2),
      var(--cubic1-3),
      var(--cubic1-4));
}
.advanced-calc .x {
  transition-timing-function: 
    cubic-bezier(
      calc(var(--cubic1-1) * var(--cubic1-change)),
      calc(var(--cubic1-2) * var(--cubic1-change)),
      calc(var(--cubic1-3) * var(--cubic1-change)),
      calc(var(--cubic1-4) * var(--cubic1-change)));
}

如果您在某些浏览器中查看此内容(或者想知道为什么此示例具有 .advanced-calc 类),您可能已经怀疑这种方法存在问题。 的确,有一个重要的注意事项……calc 的魔力并不总是跨浏览器按预期工作。 Ana Tudor 长期以来一直讨论着 浏览器对 calc 的支持差异,而我则对其他一些 简化的 calc 用例 进行了一些额外的测试。

好消息:所有支持自定义属性的浏览器在将单位转换为 pxvminrem 和其他线性距离单位时,通常都可以在诸如 widthtransform: translate() 这样的属性中与 calc 配合使用。

坏消息:Firefox 和 Edge 在处理其他单位类型(如 degms 甚至某些情况下 %)时,通常会出现问题。 因此,之前的 filter: hue-rotate()--rotation 属性将被忽略。 它们甚至在某些情况下无法理解 calc(1 * 1),因此即使保持无单位(例如在 rgb() 中)也可能成为问题。

虽然所有支持自定义属性的浏览器都允许在我们的 cubic-bezier 中使用变量,但并非所有浏览器都允许在任何级别使用 calc。 我觉得这些 calc 问题是当今自定义属性的主要限制因素……它们甚至不是自定义属性的一部分。

浏览器中针对这些问题跟踪着 bug,您可以使用渐进增强来解决它们。 之前的演示仅在知道可以处理它们的情况下才会进行 cubic-bezier 修改,否则您将获得基线值。 它们将错误地通过 CSS @supports 检查,因此需要 JS Modernizr 式的检查

function isAdvancedCalcSupported() {
  document.body.style.transitionTimingFunction = 'cubic-bezier(calc(1 * 1),1,1,1)';
  return getComputedStyle(document.body).transitionTimingFunction != 'ease';
  //if the browser does not understand it, the computed value will be the default value (in this case "ease")
}

通过 JavaScript 进行交互

自定义属性在 CSS 中非常有用,但通过 JavaScript 进行交互可以释放更多能力。正如 `cubic-bezier` 演示中所示,我们可以用 JavaScript 写入新的属性值。

var element = document.documentElement;
element.style.setProperty('--name', value);

这将为全局定义的属性设置一个新值(在 CSS 中,在 `:root` 规则中定义)。或者你可以更直接地为特定元素设置一个新值(因此它对该元素具有最高特异性,并且变量对于使用它的其他元素保持不变)。当你在管理状态并需要根据给定值修改样式时,这很有用。

David Khourshid 在 关于可观察对象的背景 中讨论了使用 JS 与自定义属性交互的强大方法,它们确实很好地融合在一起。无论你想要使用可观察对象、React 状态变化、久经考验的事件监听器,还是其他方式来推导出值变化,现在都打开了一扇通往两者之间交互的大门。

这种交互对于接受多个值的 CSS 属性尤其重要。我们长期以来一直使用 `style` 对象从 JavaScript 修改样式,但只要我们需要修改一个长值的其中一部分,就会变得很复杂。如果我们需要在 `background` 规则中定义的十个背景中更改一个背景,我们必须知道要修改的是哪一个,然后确保其他九个保持不变。当尝试仅修改 `rotate()` 并保持当前 `scale()` 不变时,`transform` 规则会变得更加复杂。使用自定义属性,你可以使用 JavaScript 来单独修改每一个属性,从而简化整个 `transform` 属性的状态管理。

查看 Dan Wilson 的 CodePen 作品 六边形和变量的舞蹈 (@danwilson)。

无单位的方法在这里也很有效。你的 `setProperty()` 调用可以将原始数字传递给 CSS,而不是必须附加单位,这在某些情况下可以简化你的 JavaScript 代码。

现在该使用它了吗?

由于自定义属性现在已包含在 Mozilla、Google、Opera、Apple 和 Microsoft 的最新浏览器中,因此现在绝对是探索和实验的好时机。这里讨论的许多内容现在都可以使用,并可以使用合理的回退机制。一些浏览器中需要的 `calc` 更新将更晚出现,但你仍然可以在某些情况下合理地使用它们。例如,如果你在仅限于较新 iOS、Android 或 Windows 版本的混合移动应用程序上工作,你将有更大的空间发挥。

自定义属性是对 CSS 的重大补充,可能需要一段时间才能弄清楚它的工作原理。先试着尝试一下,如果适合你,再深入研究。