本文探讨了为什么人们在CSS方面表现不佳。文章提到CSS的复杂性和不断变化的标准是导致问题的主要原因。作者还讨论了开发者和设计师之间的沟通问题,以及缺乏足够的培训和教育。他提到了一些常见的CSS错误,例如盒模型和浮动,以及如何避免它们。
下面是正文~~
许多开发人员一想到 CSS,就会想到彼得-格里芬(Peter Griffin)试图打开百叶窗。但对其他人来说,CSS 更像是把手伸进《沙丘》中的痛苦之箱,而某个产品经理却拿着匕首抵着他们的脖子,让他们不敢把手抽出来。
有几个原因可以解释为什么科技公司在 CSS 方面一直举步维艰。
- 我们不擅长教授 CSS。虽然有大量优秀的 CSS 实践者在分享他们的知识(如 Stephanie Eckles、Kevin Powell 和 Adam Argyle 等),但很多人在大学或训练营中学习 HTML 和 CSS,而这些人的知识可能并不渊博,他们使用过时的技术,或者为了偏爱 Bootstrap 或 Tailwind 等框架而忽略了基础知识。因此,很多人对 HTML 和 CSS(网络的基本构件)的了解并不深入。
- 我们不擅长招聘 CSS。几乎每个全栈或前端工程师的招聘信息都会将精通 HTML、CSS 和 JavaScript 作为必备条件,但在面试求职者时,他们很少会测试 JavaScript 以外的技能。如果公司最终录用了掌握 CSS 技能的人,那通常是偶然的。如果你没有掌握这些技能的人,你就无法审查其他人是否掌握这些技能,问题就会一直延续下去。
- 我们不擅长编写 CSS。由于缺乏对 CSS 的深入了解,又无法聘请到具备这方面知识的人才,人们不得不通过依赖 Bootstrap/Tailwind 或尝试使用 JavaScript 来完成所有工作,来避免编写 CSS。最终,他们把事情搞得过于复杂,导致 CSS 极难维护。
编写 CSS 就是将同一套视觉样式反复应用于各种不同的环境中,直到你死去
尽管 CSS 技术取得了最新进展,但许多人仍停留在这种 BEM 思维模式中,试图完美地封装一切,以免在进行更改时出现意想不到的结果。
以 BEM 文档中的这个例子为例:
.page__header {
padding: 20px;
}
.page__footer {
padding: 50px;
}
这实际上与使用 Tailwind 等框架中的实用工具类并无太大区别,只不过在任何其他情况下,你都不会使用 page__header 为元素添加 20 像素的 padding 。
使用 Tailwind 的 "原子优先"方法,你需要为每一个单独的设计决策应用一个类,这样就会产生像他们网站上的这个例子一样的标记:
“Tailwind CSS is the only framework that I've seen scale
on large teams. It’s easy to customize, adapts to any design,
and the build size is tiny.”
Sarah Dayan
Staff Engineer, Algolia
我们基本上是将这些相同的上下文设计决策(在这个例子中,就是这张卡片看起来如何)转移到标记中的类名上,而不是在我们的CSS中添加新的类名。
那么,答案是什么呢?
我们希望我们的风格足够通用,可以在不同的语境中重复使用,但又不会太通用,以至于我们不得不在这些语境中不断重复自己的风格。
简而言之,我们的想法是用单个类为单个组件设计样式,用实用工具类在不同上下文中组成或修改组件,并提供布局以保持页面之间和页面内部的一致性。
酷酷的样子
让我们重构 Tailwind 网站上的卡片示例。
这张卡片包含一个推荐信,但我们可能想在不同的上下文中使用这种卡片模式。我们的卡片不应关心其内部的内容。也就是说,在这个特定的卡片示例中,我们不会使用 .card- 对所有内容进行限定。这些样式只决定了卡片容器的外观。
/* /scss/components/_card.scss */
.cool-card {
border-radius: $radius-medium;
background-color: $color-surface-brand-light;
overflow: hidden;
}
@media (prefers-color-scheme: dark) {
.cool-card {
background-color: $color-surface-brand;
color: $color-text-inverse;
}
}
这里使用的是 SCSS 变量,而不是 CSS 标记的自定义属性。我喜欢自定义属性,但有争议的是,我不喜欢使用标记。
我们的设计系统不仅定义了我们使用的特定值(颜色、类型、间距),还定义了我们使用这些值的上下文。我们不给开发人员提供允许他们应用任何颜色的实用类(例如 .bg-slate-100 ),我们只希望在特定的上下文中使用特定的颜色。
每当我看到一个 mixin 会对调色板中的每种颜色进行排查,并为每种颜色创建一个背景色实用工具类时,我都会感到恶心。你永远不会用到每一种颜色,如果你提供了这样的选项,你最终会得到一些缺乏足够对比度的颜色组合。
这就是为什么我使用单独的标记层来定义上下文。 $color-surface-brand-light 可能指向 $slate-100 。如果我们想更改我们的品牌颜色用于背景的值,我们可以更改一个标记,将其应用于不同的组件,而无需查找 $slate-100 的每个实例并将其替换为不同的颜色。
与其让开发人员访问所有令牌,不如将它们抽象到我们的类中,开发人员可以根据不同的上下文使用相应的类。
此外,由于我们使用的是 SCSS,因此我们可以在标记名上使用更多字数,因为无论如何,它们都会编译成更小的值。
这个特定卡片中的内容包括一张图片和一个块状引文,使用 flexbox 水平排列。让我们添加一个 flex 工具。
/* /scss/utilities/_flex.scss */
.cool-flex {
--flex-align: center;
--flex-gap: $spacing-16;
display: flex;
align-items: var(--flex-align);
gap: var(--flex-gap);
}
在这里,我们在我们的弹性布局(flex)工具中使用CSS自定义属性,以便从我们的设计系统中提供一些常见的默认值。这样,我们就不需要提供一大堆额外的工具类来支持每个弹性布局属性的所有可能值。
如果开发者遇到需要覆盖默认设置的情况,他们可以通过在样式属性(style attribute)中声明来实现这一点。在这种情况下,我们不希望图片和引用块(blockquote)之间有间隙,因为这将由内边距(padding)来处理。
...
当然,我们可能还想使用其他灵活的属性,但我坚信需要时才添加,而不是试图考虑所有可能的使用情况。就这张卡而言,这已经足够了。
在本设计中,flex 只在视口宽度超过一定值时才会应用,因此我们可以创建另一个只在某个断点以上应用的 flex 工具。
/* /scss/utilities/_flex.scss */
@media (width >= $breakpoint-medium) {
.cool-flex-responsive {
--flex-align: center;
--flex-gap: $spacing-16;
display: flex;
align-items: var(--flex-align);
gap: var(--flex-gap);
}
}
我从未真正开发过需要一个以上断点的系统(也许有些布局需要断点,但单个组件不需要),因此我倾向于使用 -responsive 来表示只应在某个断点之上发生的事情。随着组件查询得到更广泛的支持,基于视口的媒体查询在类似情况下可能很快就不需要了。
现在,我们还可以在常青树浏览器中使用新的范围语法进行媒体查询!我们可以使用 width >= $breakpoint-medium 代替 max-width: $breakpoint-medium 。
图像
当设计师在大屏幕和小屏幕之间采用完全不同的设计时,我有点抓狂。我会尽我所能让它发挥作用。
在这里,我们的图像会从一个小圆圈变成大屏幕上的全尺寸图像。这可能需要一个独特的组件。
/* /scss/components/_avatar.scss */
.cool-avatar {
width: $avatar-medium;
height: $avatar-medium;
border-radius: $radius-round;
object-fit: cover;
}
@media (width >= $breakpoint-medium) {
.cool-avatar {
--width: 100%;
max-width: var(--width);
width: auto;
height: auto;
border-radius: 0;
}
我们为小屏幕上的圆角头像大小添加了一个标记,并设置 object-fit 以考虑到长宽比不是正方形的图像。在大屏幕上,我们使用自定义属性来覆盖图像的宽度。
实际上,我们必须将 .cool-flex 的 --flex-align 属性重新设置为默认的 stretch,以支持引用块(blockquote)中的文本高度超过图片的情况。因此,我们的 --width 属性实际上是设置了最大宽度,而宽度和高度都设置为自动,由图片的宽高比来决定。为了补偿这一点,我在文本容器中内联添加了一个 align-self: center。(这是针对一个非常具体的设计选择需要考虑的很多事情,但这种情况确实会发生。)
我们还需要考虑头像在小屏幕上的定位问题。这就需要一些只出现在小屏幕上的实用类。是的,这些类名有点冗长,但我觉得它们比 md:h-auto 更清晰,而且还利用了逻辑属性。
/* /scss/utilities/_spacing.scss */
@media (width < $breakpoint-medium) {
.cool-margin-auto-on-small {
margin-inline: auto;
}
.cool-margin-block-start-on-small {
--size: $spacing-32;
margin-block-start: var(--size);
}
}
文本容器
包含我们的引用块(blockquote)和图像标题(figcaption)的容器应用了一些内边距(padding),同时元素之间也有一些外边距(margin),并且在小屏幕上文本是居中的。现在是时候添加更多工具类了!
/* /scss/utilities/_spacing.scss */
:where(.cool-flow) {
--flow-size: $spacing-16;
& > :not(:last-child) {
margin-block-end: var(--flow-size);
}
}
.cool-inset-square-32 {
padding: $spacing-32;
}
/* /scss/utilities/_text.scss */
@media (width < $breakpoint-medium) {
.cool-text-center-on-small {
text-align: center;
}
}
我已经将它包含在一个 :where() 伪类函数中,以将其特异性降低到零,这样你就可以在需要时使用另一个工具类来覆盖任何子元素的底部外边距。
文本
在 Tailwind 的版本中,他们应用了 .text-medium 来设置 blockquote 文本和其下方 figcaption 的字体权重。我们可以使用类似的类,将其应用于整个容器,但在这种情况下,我们可以让字体权重继承自 body 。
然后我们需要一种用于大文本的文字样式,以及我所说的“柔和文本”样式——这种文本使用较低对比度的颜色来表示其重要性降低,而不是通过调整字体大小或字体粗细来实现。
还有一些蓝色文字看起来像链接,但其实不是。我假设这实际上是一个链接,在这种情况下,我们可以在全局样式中为链接应用 .cool-text-interactive 样式,这样我们就可以直接使用不带类的 。
/* /scss/components/_text.scss */
.cool-text-large {
font-size: $text-large-font-size;
line-height: $text-large-line-height;
}
/* /scss/utilities/_text.scss */
.cool-text-interactive {
color: $color-text-interactive;
}
.cool-text-subdued {
color: $color-text-subdued;
}
@media (prefers-color-scheme: dark) {
.cool-text-interactive {
color: $color-text-interactive-inverse;
}
.cool-text-subdued {
color: $color-text-subdued-inverse;
}
}
完成后的标记
下面就是我们重构后的标记。
“Tailwind CSS is the only framework that I've seen scale
on large teams. It’s easy to customize, adapts to any design,
and the build size is tiny.”
Sarah Dayan
Staff Engineer, Algolia
乍一看,这并不比 Tailwind 示例简洁多少,直到你实际查看了 Tailwind 示例的源代码,看到了他们实际使用的所有实用类和内联样式,而这些在代码示例中并没有显示出来。这里仅以图片元素为例:
不过,最终的代码总体上减少了类的数量,更容易解析类的作用,而且在不同的上下文中重复使用这些样式时可以减少重复。
完整事例:https://codepen.io/peruvianidol/pen/VwEqERR?editors=1100。