代码即文档:使用 CSS 网格的新策略

Avatar of Sally McGrath
Sally McGrath

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始,获得 200 美元的免费赠金!

我在 Supercool 工作,这是一家快速发展的为艺术客户定制网站的设计机构,由现成的系统 Craft CMS 提供支持;它具有高规格的图形设计,以及相对苛刻的排版和艺术指导。 在过去的几个月里,我们一直在转向 CSS 网格。 我们正在缓慢地过渡,让自己发现新的范例和设计方法,而不是简单地将旧习惯移植到新的语法中。

到目前为止,我们已经开发了许多非常有用的策略来跟踪布局。 我写了一些非常漂亮的混合,使用命名区域和模板,我们已经找到了一些基本约定来创建高度可读的代码。 我认为,使用网格逐步完成一个完全开发的生产组件的实现过程将非常有价值,深入探讨它提出的一些设计问题,并引导您避开我们遇到的一些陷阱。 CSS 网格是一个庞大的规范,有很多可能的方法和很多正确的方法来做事,但在某些时候,您必须锁定您的方法并将其上线。

我希望您对 CSS、Sass、BEM 有基本的了解,并对在紧迫的时间(例如,一周)内从 SketchPhotoshop 类型的文档中原型化完全实现的、可访问的、自定义框架(包含 50 多个组件)的任务感兴趣。

首先,让我们确定设计并将设计分成不同的编码任务,并计划如何处理它们

  1. 字体: 设计师已经定义了一个字体系统。
  2. 颜色: 首先,我们构建一个主题模型,然后将其包含在局部中。
  3. 内容: 此块中有哪些元素? 它有哪些变化形式? 这就是我们 BEM 混合的用武之地。
  4. 布局: 这是内容在块中的放置方式。 您可能想直接跳到此处。
  5. 约定: 这正是我们选择如何编写上述所有内容的方式。 在 CSS 中有很多正确的答案,所以重要的是我们都同意一个约定,即道路规则。 这实际上是首先要做的,但为了这篇文章,我们将在最后总结。

字体系统

我们对字体样式使用实用程序类(例如 h-text--h1h-text--badge)。 一个项目中可能有一种字体样式。 我们使用 Typex 将这些样式从 Sketch 直接导出到我们的 Patternlab 中。 这本身就是一整篇文章,所以让我们把类型规定为已处理。 我们不会在组件局部中引入类型。

颜色使用

查看 limograf (@Sally_McGrath) 在 CodePen 上的代码笔
CSS 变量回退混合 v2

CodePen 上。

主题化是一些很小的混合,所以我们在局部中理想情况下不会看到大量的颜色规则。 我们将它们全部存储在我们“混合和模型”库中的 _themer.scss 局部中,这样我们就可以确保遵循网站的设计系统。 这样,当有人稍后返回构建时,他们就会有一个描述设计和品牌规则的关键参考部分。 当在同一个市场中构建和维护大量网站时——但每个网站都有不同的品牌规范——你必须确保你没有将一个品牌与另一个品牌混淆! 因此,就像类型一样,我们将颜色规则从局部中抽象出来。 从本质上讲,我们实际上只在 _header.scss 文件中查看布局(尽可能多)。

鉴于我们同意始终使用我们的混合进行主题化的约定,这就是它将如何包含在元素上的方式

@include var($property, $value);

然后我们将设置一个主题模型,说明颜色如何在这个特定站点上工作,并使用以下方法将该主题应用于组件

@include theme;

这是我们将在此页面标题中使用的示例主题模型。 非常简单。

查看 limograf (@Sally_McGrath) 在 CodePen 上的代码笔
主题模型

CodePen 上。

我们正在将颜色与黑色或白色配对。 我们依赖于对比度规则并翻转它们以强调,可能是在事件上,例如悬停或突出显示的号召性用语。 这就是我们实现这一目标所需的全部,现在我们有了一份关于颜色在这个网站上应该如何真正工作的文档。 如果我们需要调试或扩展 UI,我们可以去查看并对照。

我们还想准备继承来帮助我们,所以让我们确定一些有用的约定

  • 在您的管道中将 SVG 图标上的填充设置为 currentColor(并在 CSS 中将它们默认大小设置为 width: 1em; height: 1em; font-size: inherit;,同时我们也在处理它)。
  • 在基准处将 <body><a> 设置为 currentColor)。
  • 编写简写形式,继承边框(例如 1px solid1px solid currentColor)。

使用此主题模型,我们可以生成任意数量的主题,可能将它们存储为实用程序类,或者循环遍历组件内的修饰符列表,或者只是允许用户在 CMS 中直接在块上设置变量。 当 IE 11 在我们的统计数据中低于 1% 时,我们可以使用变量做更多的事情,但这对于我们目前的目的来说已经足够了。

让我们不要跑题。 网格呢?!

内容组件

网格让我们以一种新的方式准确描述每个局部中我们拥有什么内容。 对于为每个项目构建新的 UI 的设计机构来说,这确实改变了游戏规则,并且随着我们的探索,我们正在发现它新的(和有趣的)应用程序。

背景介绍:我们使用 Craft CMS 为我们的客户定制每个界面,并根据他们的特定需求和内容模型创建自定义字段。 我们有内部工具可以从票务 API 中提取事件并根据这些数据创建条目,然后可以在 CMS 中编辑和扩展(或完全创建)这些条目。 客户可以在永久页面区域中填写或编辑命名字段,并在构建每个页面时将整个设计好的、品牌化的内容块添加到布局中。

很多 UI。 客户可以对内容进行大量控制,而我们可以对 HTML 进行大量控制,因此我们可以确保页面上的可访问语义代码的高标准。 我们在发现过程中共同开发内容模型,然后让他们自由地创建内容。 他们添加他们想要的东西,我们确保它有效并且始终看起来正确。 比正确更好! 超级。 (对不起! :P)

所以,作为一名开发人员,我必须平衡相互竞争的优先事项

  • 可访问性、可用性
  • 品牌和图形设计
  • 性能
  • 维护和代码库健康

让我们逐一看看

可访问性

可访问的、逻辑的 HTML 是我的最爱。 至少,我的项目需要在 Lighthouse 得分上获得绿色可访问性得分。 (我在和谁开玩笑,我想要那个美味的 100 分!)核心路径和页面使用几个屏幕阅读器、键盘 标签、键盘导航)、低视力模拟器dasher、语音访问和 二进制开关 进行测试。 (我也在 Robots and Cake 工作,所以这是我开发工作的重要组成部分。)我一遍又一遍地向页面添加巨大的可点击电话号码和电子邮件地址。 我只是想让人们到达他们想去的地方。

我一直担心内容可以通过网格(以及 flexbox)重新排序的方式。 现在已经完成了几个版本,我实际上认为网格可以帮助我们解决这个问题。 使用 CSS 网格,没有理由为了布局而移动 HTML。 我们可以回到将整个文档视为逻辑上的线性序列作为我们的首要考虑因素。

品牌与性能与维护

艺术场馆需要高规格的图形设计,在印刷和网络上统一,并且需要不断更换材料(例如,节目单、小册子、门票、海报、微型网站等)以吸引观众,包括必须履行的合同营销义务。 可以想象,我们有很多高质量的大图像需要优先考虑,并且通常带有强烈的印刷主导品牌。 这意味着我们可能会向页面提供大约 15 种自定义字体(包括粗细变化、显示字体等)和复杂的 CSS。 我们必须尽可能保持精简。 我们目前正在发布大约 20 KB nano Gzipped 的 CSS,但我正在努力进一步减少它。

但是,我们确实通过在 PostCSS 任务中设置 reduce identifiers to false 来保持网格区域名称的完整长度。在开发工具中提供布局图比节省那几个字节要实用得多。为了便于维护、自我文档以及为了将来在索尔比桥的晚班火车上无法访问代码仓库的情况下调试此站点的您自己:请保留这些地图。

代码健康

平衡所有这些相互竞争需求的方法是阐明并商定约定,以便在测试中减少需要修复的内容,并确保*已解决的问题保持解决*。我们检查我们构建的所有组件,并确保它们始终以标题开头,链接可以链接到其他地方,按钮可以触发操作,可计数的对象以列表形式传递并在前面加上地标标题,导航是 <nav> 并且时间是 <time> 并且 div 汤在早餐时吃——基础知识。

使用 CSS 网格,**没有理由为了布局而移动** **HTML**。您的内容始终可以逻辑地流动,而布局的变化发生在 CSS 中。而且,由于不需要边距或填充来创建装订线,您可以简单地声明

.o-grid .o-grid { width:100%; }

…确保任意数量的嵌套组都在视觉上占据相同的页面网格。HTML 可以更清楚地指导事物的本质:更接近的文档。

参见由 limograf (@Sally_McGrath) 编写的 Pen
锁定语义可访问结构

CodePen 上。

标题和操作之间有很多内容需要管理,我的挑战是跟踪所有这些组件中的所有这些字段,*并*使其可遍历、可扫描、可线性化,并以某种逻辑、易懂的方式轻松阅读,*同时*确保我忠实地执行设计规范。

让我们引入我的第一个令人惊讶的有用的网格 mixin。

@mixin template($elements...) {
  @each $element in $elements {
    &__#{$element} {
      grid-area: $element;
    }
  }
}

在任何地方使用此 mixin 意味着

  1. 每个组件部分现在都以其所有可能元素的列表开头,这是一份非常方便的文档,尤其是在Twigging 实际的前端组件时。
  2. mixin 负责分配网格区域。
  3. 元素和组件名称在 Sketch、CSS 和 HTML 中保持一致,并且任何不一致之处都将非常明显,因为布局会失败。我坚定但公平。
  4. BEM 命名是自动强制执行的,但不会混淆部分中的内容。

现在,在部分中,我们将只声明 grid-template-areas,使用正常的英文单词,为我们提供一系列布局图,这些布局图也与数据库字段相匹配。超级易读!

以下是此 mixin 工作的示例

参见由 limograf (@Sally_McGrath) 编写的 Pen
BEM 元素自动分配

CodePen 上。

我们决定坚持对内部网格使用命名区域,因为我阅读了一篇关于这个网站的好文章,解释了如果您坚持列出的受支持属性,Autoprefixer 如何处理 IE 11 的网格 - 它在大多数情况下都是这样做的。如果您在浏览器测试中以超级有用的调试模式查看应用了 Autoprefixer 的测试用例,您会看到它正在工作。

到目前为止,一切都很好。

但也有陷阱!您必须**将内联元素设置为块**,以确保它们始终在 IE 11 中作为网格单元格运行。注释掉示例中标记的行以查看否则会发生什么

调试发现了一个问题。

哎哟!小心那些街区。您可能会发现某些版本的 IE 11 甚至没有选择此修复程序,在这种情况下,您可能会尝试使用普通的 <p> 标签……叹息。

我没有在此 mixin 中包含 display: grid,因为在某些情况下,实际网格是设置在内部容器上的,例如,但我们仍然希望网格区域在正确的 BEM 类上匹配。

所以

.c-header{ 
  @include template(title, pretitle, posttitle, producer, venue, credit, quote, nav, infobar, search);
}

让我们把这些家伙放出去。

布局

让我们确定更多规则,以确保该组件顺利地滑入页面布局中。在撰写本文时,没有子网格)(但以后会有!),所以这个组件对其所在的父网格一无所知。这恰好与 BEM 组件方法相匹配——因为每个组件都是扁平的、孤立的,以限制继承。我在这里不是在提倡 BEM(或者我们显然使用的 BEM-*ish*)——我只是说,如果您已经在使用它,这是一个好处。

在这个例子中,设计师在整个站点设置了一个 12 列网格的页面布局,间距为 20px(1.25rem),没有偏移块。我们的组件是一个页面区域,将占据所有 12 个网格列。在这个过渡时期,我们仍在使用这种设置的网格,因为我们有大量基于这个想法的系统必须与之集成。所以,以下是我们针对这种情况的约定:对于全宽区域,**删除握持间隙**并将网格模板*列*写成 12 的分数单位 (fr)。

这样做意味着

  1. 此内部网格的视线大致与其所在网格一致;
  2. 很容易在代码中看到底层设计规则;并且
  3. 如果需要,很容易*精确*地对齐事物。

关于“对齐”的快速说明

等等……我说的“让事物*精确*对齐”是什么意思?它不是*已经*精确对齐了吗?

两个相等的列在父 12 列网格的中间沟槽处分开。

嗯,没有。分数单位方法完美地划分了*空间*,因此您最终会进入装订线。两列均匀的列会让您落在装订线的中间。两列中一列为 2/3,另一列为 1/3 将在该装订线中分割 1/3,依此类推。

两列不相等(分别设置为 2fr1fr)的列在 12 列父网格的装订线中分割了三分之一。

修复对齐方式并不完全是*困难*,因为我们知道页面网格装订线的宽度。例如,在平均分割时,我们可以包含网格间隙。

但是,我们无法对任何其他部门这样做。我们*可以*做的是添加该间隙作为边距 - 无论您设置了什么样的盒子大小,边距都添加在内部。在这个例子中,我们有三列(两个命名区域和一个空白区域),将我们的装订线分成三等份

以下是计算这些边距的方法:确保总的 fr 单位总和结果为 12。将网格间隙除以父网格中的列数,然后像这样相乘

n 的右边距乘数等于 n 右侧 fr 单位的总和。n 的左边距等于 n 左侧 fr 单位的总和。

因此,对于值为 2fr 3fr 2fr 4fr 1frgrid-template-columns

 2      3      2     4    1 
0/10   2/7    5/5   7/1  11/0

参见由 limograf (@Sally_McGrath) 编写的 Pen
内部名称规范和外部数字规范 - 页面区域,桌面

CodePen 上。

如果您发现自己经常编写 calc(),您甚至可以将其编写为 mixin。类似这样的东西,用于将内部网格与父网格对齐

参见由 limograf (@Sally_McGrath) 编写的 Pen
自动将内部网格与父网格对齐

CodePen 上。

…当名称在网格内部指定但在网格外部指定数字时,类似这样的东西可以自动计算边距

参见由 limograf (@Sally_McGrath) 编写的 Pen
内部名称规范和外部数字规范 - 自动计算边距

CodePen 上。

我相信您还能想到其他解决方案,例如切换到命名行,或添加额外的固定宽度列,甚至编写每行 12 个命名区域的所有地图。您可以通过多种方式来解决这个问题,但我认为其中很多方式都消除了命名区域的优势。区域为我们提供了一个*可读*的布局图,其中包含我们未来需要知道的内容。它是作为文档的代码。

需要说明的是,我正在向我们介绍的设计问题不是对齐问题。使用网格很容易对齐。问题不在于解决眼前的、微不足道的布局问题,而在于以一种支持我们目标的方式解决它,即能够在六个月后回来并掌握

  1. 组件中有哪些元素。
  2. 它们是如何布局的。
  3. 代码为什么以这种方式编写。

网格规范非常庞大,很容易迷失在选项中。也许更好的计划是重置为 12 列网格并在需要绝对对齐时使用数字规范(即明确链接到我们使用数字规范的页面网格)——但我确实觉得有一个更智能、更简单的解决方案等待被发现。对于这个网站,我们最终编写了一个页面网格对象,并在其中添加了嵌套的内部网格单元格:.o-page-grid__sidebar

大家怎么看?我确实预见到对此会有不同的看法。🤦‍♀️

一个真实的、实时的网格!

我们可以用它来创建一个通用的页面标题

参见由 limograf (@Sally_McGrath) 编写的 Pen
01 - 通用页面标题

CodePen 上。

或者,我们可以创建主页的变体

查看 limograf (@Sally_McGrath) 的 CodePen 作品
02 – 首页

CodePen 上。

想要一个突破容器限制的英雄页眉?没问题!我们也可以在容器外实现它。

查看 limograf (@Sally_McGrath) 的 CodePen 作品
03 – 英雄

CodePen 上。

接下来是什么?一个主题活动页眉,带有全宽信息栏(固定显示)和一个与父网格侧边栏对齐的内部按钮?当然可以。我会包含一个父网格,以便更容易理解。

查看 limograf (@Sally_McGrath) 的 CodePen 作品
04 – 活动页眉

CodePen 上。

想要一个居中对齐的搜索框?让我们使用一种折叠列的技术。

查看 limograf (@Sally_McGrath) 的 CodePen 作品
06 – 居中对齐的搜索框

CodePen 上。

这是一个演示,展示了所有这些变体作为一个部分。是的,它是一张地图!到此为止!

规范

呼,我们涵盖了很多内容!但你可以看到像这样的系统是多么灵活和易于理解,对吧?

  1. 排版使用单独的排版系统处理。
  2. 颜色由主题部分处理,该部分描述了设计的底层颜色规则,而不是简单地随意为元素着色。
  3. 元素使用英文命名,并在部分顶部以模板混合的形式列出。此列表可以在 Twig 或模板中作为参考。
  4. 始终使用正确的 HTML,并且嵌套不会破坏网格。这意味着你可以通过设置规范将任意数量的嵌套网格应用于同一个布局空间。
  5. 精确对齐是在数字规范中完成的,而不是在名称规范中完成的(但请注意,可以使用名称规范进行对齐)。
  6. 支持 IE 11。
  7. 我还有一个快速说明和另一个使用命名区域构建的组件示例。在这个例子中,卡片不是区域,而是 放置在网格中 的组件,所以没有理由使用 12 列的 fr 规范。你可以期望媒体对象部分看起来像这样。

    .c-card {
      &--news {
        align-content: start;
        grid-template-areas: 
          "image"
          "datetime"
          "title";
      }
    
      &--search {
        justify-content: start;
        grid-template-columns: 1fr 3fr;
        grid-template-areas:
          "image page"
          "image title"
          "image summary";
      }
    
      &--merchandise {
        grid-gap: 0;
        grid-template-columns: $b 1fr 1fr $b;
        grid-template-areas:
          "image image   image   image"
          ".     title   title   ."
          ".     summary summary ."
          ".     price   action  .";
      }
    
      &--donations {
        // donations thanks button is too long and must take up more space than input
        grid-gap: 0;
        grid-template-columns: $b 1fr 2fr $b;
        grid-template-areas:
          "image image   image   image"
          ".     title   title   ."
          ".     summary summary ."
          ".     input   action  .";
      }
    }
    
    // ...