CSS 精通指南(四)

原文:CSS Mastery

协议:CC BY-NC-SA 4.0

九、设计表单和数据表的样式

表单是现代 web 应用程序中极其重要的一部分。它们允许用户与系统交互,使他们能够做任何事情,从留下评论到预订复杂的旅行路线。表单可以像电子邮件地址和消息字段一样简单,也可以非常复杂,跨越多个页面。

除了需要捕获用户数据,web 应用程序越来越需要以一种易于理解的格式显示这些数据。表格可能是显示复杂数据的最佳方式,但需要仔细设计以避免过于庞大。组成表格的元素集合是 HTML 中比较复杂的部分之一,很容易出错。

表单和数据表的设计相对来说被忽略了,而倾向于更高层次的设计。然而,好的信息和交互设计可以成就或毁灭一个现代的网络应用。

在本章中,您将了解

  • 创建有吸引力和可访问的数据表

  • 让表格为响应式布局服务

  • 创建简单和复杂的表单布局

  • 设计各种表单元素的样式,包括复选框和选择菜单的定制样式

  • 提供可访问的表单反馈

样式数据表

表格数据是可以按列和行排列的信息。一个月的日历视图是可以标记为表格的一个很好的例子。

即使是相对简单的数据表,如果包含多行和多列,也很难阅读。如果数据单元格之间没有分隔,信息会模糊在一起,导致布局混乱(见图 9-1 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-1。乍一看,紧凑的数据表可能非常令人困惑

相反,有大量空白的表格也很难阅读,因为列和单元格开始失去彼此之间的视觉关联。当你试图跟踪具有很大列间距的表格上的信息行时,这尤其成问题,如图 9-2 所示。如果不小心,在列之间移动时很容易不小心误入错误的行。这在桌子中间最明显,桌子顶部和底部的硬边提供了较少的视觉锚。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-2。间距很大的表格也很难立即理解

幸运的是,通过应用一些基本的设计技术,可以大大提高数据表的可读性。图 9-3 中的日期被赋予了一点行高和默认宽度的喘息空间。表头用不同的文字样式和一个边框清晰区分,涉及当前日期的各种状态和周末是哪几天都有清晰的标注。结果是一个易于使用的日历小部件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-3。样式数据表

特定于表的元素

如果表格对视力正常的用户来说很困难,那么想象一下,对于使用辅助技术(如屏幕阅读器)的人来说,表格是多么复杂和令人沮丧。在最基本的层次上,表格是从 table 元素创建的,由 tr 元素(表格行)和 td 元素(表格单元格)组成。幸运的是,HTML 规范包含了更多的元素和属性,旨在提高数据表的可访问性。

表格标题

表格标题元素充当表格的标题。尽管不是必需的元素,但尽可能使用标题总是一个好主意。在本例中,我们使用标题向用户显示他们正在查看的月份:

<table class="cal">
  <caption><strong>January</strong> 2015</caption>
</table>
thead、tbody 和 tfoot

使用 thead、tfoot 和 tbody 可以将表分成逻辑部分。例如,您可以将所有的列标题放在 thead 元素中,这为您提供了一种单独设置特定区域样式的方法。如果选择使用 thead 或 tfoot 元素,则必须至少使用一个 tbody 元素。在一个表中只能使用一个 thead 和 tfoot 元素,但是可以使用多个 tbody 元素来帮助将复杂的表分成更易于管理的块。

行和列的标题应该标记为 th 而不是 td。可以给表格标题一个范围属性值 row 或 col,以定义它们是行标题还是列标题。如果 scope 属性与多行或多列相关,也可以为它们赋予 rowgroup 或 colgroup 值。一周中的日期标记列,因此它们应该将 scope 属性设置为 col。

<thead>
  <tr>
    <th scope="col">Mon</th>
    <!-- ...and so on -->
    <th scope="col">Sun</th>
  </tr>
</thead>
列和列组

tr 元素提供了对整行进行样式化的目标。但是列呢?我们可以使用:n-child 来选择表格单元格,这可能会变得混乱。col 和 colgroup 元素就是为此而存在的。colgroup 用于定义一个或多个列的组,由 col 元素表示。col 元素本身没有任何内容,而是在实际表格的一个特定列中代表表格单元格。

<colgroup>
   <col class="cal-mon">
   <col class="cal-tue">
   <col class="cal-wed">
   <col class="cal-thu">
   <col class="cal-fri">
   <col class="cal-sat cal-weekend">
   <col class="cal-sun cal-weekend">
</colgroup>

colgroup 需要放在 table 元素中,在任何标题之后,任何 thead、tfoot 或 tbody 元素之前。

然后将样式应用于 col(或 colgroup)元素,而不是特定列中的所有表格单元格,例如日历中的所有星期六和星期天。可以为列设置样式的属性非常有限。您可以设置背景属性、边框属性以及宽度和可见性的样式,但仅此而已。

最重要的是,列的可见性只能有 visible 或 collapse 值,即使这样,浏览器也不太支持。值折叠应该不仅仅是隐藏,而且是折叠表格部分的维度,这在某些情况下会很方便,但这只是一些浏览器开发者似乎跳过的事情之一。

完成的表格标记

将所有这些 HTML 元素和属性放在一起,您可以创建如图 9-1 所示的日历表格的基本轮廓。

<table class="cal">
  <caption><strong>January</strong> 2015</caption>
  <colgroup>
    <col class="cal-mon">
    <!-- ...and so on -->
    <col class="cal-sat cal-weekend">
    <col class="cal-sun cal-weekend">
  </colgroup>
  <thead>
    <tr>
      <th scope="col">Mon</th>
      <!-- ...and so on one per day.-->
      <th scope="col">Sun</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="cal-inactive">29</td>
      <td class="cal-inactive">30</td>
      <td class="cal-inactive">31</td>
      <td><a href="#">1</a></td>
      <td><a href="#">2</a></td>
      <td><a href="#">3</a></td>
      <td><a href="#">4</a></td>
    </tr>
    <!-- ...and so on, one row per week... -->
    <tr>
      <td><a href="#">26</a></td>
      <td class="cal-current"><a href="#">27</a></td>
      <!-- ...and so on -->
      <td><a href="#">31</a></td>
      <td class="cal-inactive">1</td>
    </tr>
  </tbody>
</table>

我们已经用一个占位符锚元素包装了所有的日子(假设当您单击一个日期时,日历组件会将您带到某个地方或做一些事情)。我们还添加了几个类名来表示当天。cal-current)和当月之外的天数(。校准-无效)。

设置表格元素的样式

CSS 规范有两种表格边框模型:分离的和折叠的。在单独的模型中,边框放置在单个单元格周围,而在折叠模型中,单元格共享边框。我们希望单元格共享单个 1 像素的边框,因此我们将表格的边框折叠属性设置为折叠。

表格也有一个调整单元格大小的算法,我们可以通过 table-layout 属性来控制。默认情况下,使用 auto 值,这基本上是让浏览器根据单元格的内容来决定单元格的宽度。通过将其更改为 fixed,任何单元格宽度都将根据表格第一行或任何 col 或 colgroup 元素中的单元格宽度来确定。这通过 CSS 给了我们更多的控制。

接下来,我们设置字体堆栈,将表格中的所有文本居中。最后,我们将添加一个宽度和一个最大宽度来创建一个流体组件,它尽可能多地占用空间,而不会宽得令人难以接受。

.cal {
  border-collapse: collapse;
  table-layout: fixed;
  width: 100%;
  max-width: 25em;
  font-family: "Lucida Grande", Verdana, Arial, sans-serif;
  text-align: center;
}
设置表格内容的样式

基础工作已经做好了,现在是开始添加视觉样式的时候了。为了让表格标题看起来更像普通标题,我们将增加字体大小和行高。我们还会将它向左对齐,并给它一个边框,将它与表头分开。

.cal caption {
  text-align: left;
  border-bottom: 1px solid #ddd;
  line-height: 2;
  font-size: 1.5em;
}

接下来,我们将使用 col 元素为周末设置粉色背景。请记住,背景属性是您可以对整列进行更改的少数几项内容之一。我们将使用一种高度透明的颜色,这样它可以和任何背景融为一体,但是在此之前提供一个纯色的后备声明,以适应旧的浏览器。

.cal-weekend {
  background-color: #fef0f0;
  background-color: rgba(255, 0, 0, 0.05);
}

接下来,我们将对单个单元格进行样式化。所有单元格都需要多一点行高,我们将为它们提供一个宽度。默认情况下,表格有一个根据单元格内容分配空间的布局算法。由于工作日表标题的大小不同,这导致列略有不同。我们可以指定一个等于表格宽度七分之一的宽度(14.285%)来纠正这个问题。事实上,宽度只需要至少为表格宽度的七分之一—如果单元格相加超过 100%(当使用固定表格布局模型时),它们将各自按比例缩小,直到适合为止。如果我们想让单元格等宽,不管有多少个,我们可以将它们的宽度设置为 100%。虽然这是一个很方便的技巧,但是为了清楚起见,在本例中我们将宽度保留为总宽度的七分之一。你可以在这篇由克里斯·科伊尔撰写的 CSS-Tricks 文章中阅读更多关于表格布局的古怪之处:【https://css-tricks.com/fixing-tables-long-strings/

在一些浏览器中,表格单元格也有默认的填充,我们想要移除它。我们还将为表格单元格添加一个模糊的边框,但不是标题单元格。

.cal th,
.cal td {
  line-height: 3;
  padding: 0;
  width: 14.285%;
}
.cal td {
  border: 1px solid #eee;
}

为了将表格标题与表格数据(实际日期)分开,我们将添加一个更粗的边框。这应该和在 thead 元素上设置边框一样简单。

.cal thead {
  border-bottom: 3px solid #666;
}

这在大多数浏览器(Chrome、Firefox、Safari、Opera 等)中都能正常工作。)但遗憾的是在 Internet Explorer 或 Edge 中没有。表格中的边框,不管它们是在表格单元格、一行还是一组行上(例如 thead 或 tbody),都在我们选择的折叠表格模型中融合在一起。幸运的是,大多数浏览器在设置一整行的边框时都会覆盖垂直边框。IE 和 Edge 会尝试将左右两边的边框和 thead 元素上的边框连接起来,造成难看的缝隙(见图 9-4 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-4。IE 和 Edge 都让垂直边框撞上了表头的水平边框,产生了空隙

种方法可以解决这个问题。我们可以退出折叠的边框模型,为单个表格单元格的边框添加更多的规则,但是在这种情况下,我们将保持原样。如果你遇到这个问题,并需要它在 IE 中看起来和在其他浏览器中完全一样,你可能不得不求助于单独的边框模型或使用类似背景图像的东西来代替。

接下来,我们将处理日历小部件中代表可点击日期的锚链接。我们将删除下划线,给他们一个深紫色,并设置他们显示为块。这将导致它们扩展以填充整个表格单元格,从而创建一个更大的可点击区域。最后,我们将为悬停和聚焦状态添加规则,其中我们显示半透明的背景色(使用与前面相同的后退技术到纯色)。

.cal a {
   display: block;
   text-decoration: none;
   color: #2f273c;
}

.cal a:hover,
.cal a:focus {
  background-color: #cde7ca;
  background-color: rgba(167, 240, 210, 0.3);
}

最后,我们将为日历日期的其他状态添加样式。我们有不在当前月份的日期,所以我们会给它们一个褪色的颜色,并清楚地表明它们不能通过使用不同的指针来选择。

对于当前日期,我们将把背景颜色改为另一种稍微半透明的色调。各种状态的半透明颜色混合在一起,因此我们将根据我们是否有当前日期、当前日期被悬停、当前日期被悬停在“周末”内等,自动得到不同的结果颜色。,都没有任何额外的规则(见图 9-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-5。我们为悬停、当前和非活动日期添加了各种微妙的状态
.cal-inactive {
  background-color: #efefef;
  color: #aaa;
  cursor: not-allowed;
}
.cal-current {
  background-color: #7d5977;
  background-color: rgba(71, 14, 62, 0.6);
  color: #fff;
}
.cal-current a {
  color: #fff;
}

现在你有了一个漂亮的日历,如图 9-3 所示。

响应式表格

桌子本身就需要空间。它们内置了两个轴的概念,并且随着列数的增加需要更多的宽度。结果是,复杂的桌子往往需要相当大的空间,这与能够在所有屏幕上舒适地显示事物的响应目标相冲突,无论屏幕大小。

我们之前提到过,在 CSS 中,表格(以及表格的每个组件)都有自己的显示模式。我们可以利用这一点,让不是表格的东西借用表格的“网格特性”,以达到布局的目的。但是我们也可以使用相反的策略,让表格不显示为表格!我们将采用这种方法使表格数据适合较小的屏幕。

线性化表格

当我们有一个包含大量列的表格时,我们可以翻转它,使每一行都表示为一个由表格标题文本和该行的值组成的块。让我们创建一个示例表来直观显示这一点,其中包含一组汽车模型的数据。最终结果将在大屏幕上看起来如图 9-6 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-6。汽车模型的数据表,带有一些简单的样式

对于较小的屏幕,每行都有自己的块。表格标题行是隐藏的,列标签打印在每条数据之前。它看起来有点像图 9-7。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-7。小屏幕线性化的表格
<table class="cars">
  <caption>Tesla car models</caption>
  <thead>
    <tr>
      <th scope="col">Model</th>
      <th scope="col">Top speed</th>
      <th scope="col">Range</th>
      <th scope="col">Length</th>
      <th scope="col">Width</th>
      <th scope="col">Weight</th>
      <th scope="col">Starting price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Model S</td>
      <td>201 km/h</td>
      <td>426 km</td>
      <td>4 976 mm</td>
      <td>1 963 mm</td>
      <td>2 108 kg</td>
      <td>$69 900</td>
    </tr>
    <tr>
      <td>Roadster</td>
      <td>201 km/h</td>
      <td>393 km</td>
      <td>3 946 mm</td>
      <td>1 873 mm</td>
      <td>1 235 kg</td>
      <td>$109000</td>
    </tr>
  </tbody>
</table>

该表格的样式由一些简单的边框、字体规则和一种“斑马条纹”技术组成,其中表格的每一偶数行都有不同的背景颜色:

.cars {
  font-family: "Lucida Sans", Verdana, Arial, sans-serif;
  width: 100%;
  border-collapse: collapse;
}

.cars caption {
  text-align: left;
  font-style: italic;
  border-bottom: 1px solid #ccc;
}

.cars tr:nth-child(even) {
  background-color: #eee;
}
.cars caption,
.cars th,
.cars td {
  text-align: left;
  padding: 0 .5em;
  line-height: 2;
}
.cars thead {
  border-bottom: 2px solid;
}

如果我们调整屏幕大小,我们发现在大约 760 像素宽时,这个表格开始变得非常拥挤,难以阅读(见图 9-8 )。这就是我们需要放置断点并开始改变的地方。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-8。我们的桌子在大约 760 像素宽时开始变得拥挤

表格有很多默认样式和显示模式。如果我们走“移动优先”的路线,改变默认样式,然后使用最小宽度条件为更大的屏幕重置默认值,我们可能要做很多工作。这就是为什么我们将使用最大宽度条件,以在较小的屏幕上特别针对这种特殊情况:

@media only screen and (max-width: 760px) {
  .cars {
    display: block;
  }
  .cars thead {
    display: none;
  }
  .cars tr {
    border-bottom: 1px solid;
  }
  .cars td, .cars th {
    display: block;
    float: left;
    width: 100%;
    box-sizing: border-box;
  }
  .cars th {
    font-weight: 600;
    border-bottom: 2px solid;
    padding-top: 10px;
  }
  .cars td:before {
    width: 40%;
    display: inline-block;
    font-style: italic;
    content: attr(data-label);
  }
}

表格单元格现在设置为以块的形式显示,并占据 100%的宽度,在行内将它们堆叠在一起。表格标题完全隐藏。为了保持列标签和 td 元素中的单个值之间的关联,我们在标记中的每个表格单元格上插入了每个列的标签作为数据标签属性:

<th scope="row">Model S</th>
<td data-label="Top speed">201 km/h</td>
<td data-label="Range">426 km</td>
<td data-label="Length">4 976 mm</td>
<!-- ...and so on -->

我们现在可以使用:before 伪元素在每一行单元格内容之前插入这些标签。我们可以通过使用 attr()函数符号和 content 属性来获取元素属性的内容——这是一个揭示隐藏在 HTML 中的额外数据的简便技巧。为了避免在 CSS 中硬编码标签的值,标签在标记中的重复是一个很小但必要的代价。

除了一些进一步的样式更改以保持表格的可读性之外,上一个示例中还有一些其他重要的代码部分。

首先,我们将表格本身设置为显示:block。这对于演示来说不是必需的,但是有助于提高可访问性。切换表格的显示模式不应该改变屏幕阅读器对它的解释,但它确实改变了。这意味着,当标记中有一个表格,但表格单元格设置为显示为其中的常规块(通过 CSS)时,一些屏幕阅读器会感到困惑。将表格本身设置为显示为一个块似乎会触发这些屏幕阅读器将表格作为文本流来读取,这保持了内容的可访问性,尽管失去了表格的性质。accessibleculture.org 的 Jason Kiss 在 http://accessible culture . org/articles/2011/08/responsive-data-tables-and-screen-reader-accessibility/上发表了一篇很有帮助的文章,解释了各种屏幕阅读器之间的差异。

为了使这个解决方案有效,我们做的第二件事是向表格单元格添加一个 float 声明。这只是为了应对 IE9 中的一个错误,IE9 支持媒体查询,但似乎不接受将表格单元格的显示模式更改为@media 规则内部的块。但是,它确实应用了浮动,这实际上是把单元格变成了块,这是一个副作用。将单元格的宽度设置为 100%可以抵消浮动的收缩包装效果,并保证它们像块一样垂直排列。

高级响应表

为小视口线性化表格只是创建响应表格的解决方案之一。有几种方法可以解决同一个问题,老实说,这仍然是一个相当新的问题,因为这本书正在编写。没有“一刀切”的解决方案,但有一些策略可供选择。大多数依赖 JavaScript 在需要时结合 CSS 来操作标记。各种策略都是一些基本机制的变体:

  • 当屏幕太小时,为表格的列引入某种滚动机制。例如,第一列可以固定在适当的位置,并作为一个锚点,帮助您知道您正在查看哪一行,其余的列可以滚动。

  • 当屏幕变小时隐藏列,以便只显示最重要的内容。

  • 在一个单独的窗口中链接到一个更大版本的表格,用户必须依赖缩放。

  • 使得用切换机制显示和隐藏列成为可能。

如果您需要支持复杂的响应式表格场景,您可能会发现 tool Tablesaw 非常适合(www.filamentgroup.com/lab/tablesaw.html),如果没有什么可以作为设计模式的灵感的话。它是 jQuery 插件的集合,可以帮助您实现前面列表中提到的一些策略。

样式表单

表单是 web 页面的访问者实际上做其他事情而不是消费内容的地方。它可以是填写一个联系表格,写一篇要发表的文章,输入付款信息,或者最后点击“立即购买”按钮。很明显,这些都是非常有价值的活动,但是尽管非常重要,表单的设计和编码通常很糟糕。

也许其中一个原因是表单编码总是有点麻烦。它们有很多活动部件,传统上很难设计。这是因为许多表单控件被实现为用替换内容,这意味着像 select 元素中下拉菜单中的箭头这样的控件实际上并不由任何 HTML 元素表示。它更像是一个黑盒,每当你在标记中声明一个<选择>标签时,浏览器就扔在那里。这主要是为了确保与用户当前操作系统的默认 UI 控件保持一致。

但是,我们至少可以设计表单控件外观的某些部分。对于我们不能设计样式的部分,我们可以用一些创造性的编码来伪装自定义控件的外观。

表单也不仅仅是表单控件本身,所以这一节将介绍如何标记和样式化组件,使之成为一个有吸引力的 HTML 表单。应该注意的是,它绝不是 HTML 表单各个方面的全面指南——元素和属性太多了,我们无法在这里一一介绍。

一个简单的表单示例

当表单标签垂直显示在相关表单元素上方时,简短且相对简单的表单最容易填写。用户只需一步一步地向下移动表单,阅读每个标签并完成下面的表单元素。这种方法适用于收集相对简单且可预测的信息(如联系信息)的短表单(见图 9-9 ),但对于在较小的视窗(如移动浏览器)上查看表单,这也是一个非常好的基准。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-9。简单表单布局
字段集和图例

HTML 提供了许多有用的元素,可以帮助表单增加结构和意义。第一个是 fieldset 元素。字段集用于对相关的信息块进行分组。在图 9-9 中,使用了三个字段集:一个用于联系详情,一个用于评论,一个用于“记住我”偏好设置。

要确定每个字段集的用途,可以使用 legend 元素。图例的作用有点像字段集的标题,通常垂直居中显示在字段集的顶部边框上,并稍微向右缩进。默认情况下,字段集通常呈现为双边框。不同的浏览器以不同的方式实现了这种略有不同的外观。这似乎是浏览器渲染引擎中的一个特例,用普通的 CSS 属性来撤销奇怪的定位很少会有你期望的效果。当我们设计表单的时候,我们将回头来讨论这个问题。

标签

label 元素是一个非常重要的元素,因为它可以帮助添加结构,提高表单的可用性和可访问性。顾名思义,这个元素用于为每个表单元素添加一个有意义的描述性标签。在许多浏览器中,单击 label 元素会使关联的 form 元素获得焦点。

使用标签的真正好处是为使用辅助设备的人增加表单的可用性。如果表单使用标签,屏幕阅读器会正确地将表单元素与其标签相关联。屏幕阅读器用户还可以调出表单中所有标签的列表,允许他们以听觉方式浏览表单,就像视力正常的用户以视觉方式浏览表单一样。

可以通过以下两种方式之一将 label 元素与 form 控件关联起来:通过在 label 元素中嵌套 form 控件来隐式关联:

<label>Email <input name="comment-email" type="email"/><label>

或者通过将标签的 for 属性设置为与相关表单元素的 id 属性值相等来显式设置:

<label **for="comment-email"**>Email<label>
<input name="comment-email" **id="comment-email"** type="email"/>

你会注意到这个输入和本章中的大多数表单控件都包含了一个 name 属性和一个 id 属性,因为我们通常不会将输入嵌套在标签中。id 属性是创建表单输入和标签之间的关联所必需的,而 name 属性是必需的,这样表单数据就可以发送回服务器。id 和名称不必相同,但是为了保持一致性,尽可能保持它们相同是一个方便的约定。

与使用 for 属性的表单控件相关联的标签不需要靠近源代码中的那些控件;它们可能在文档中完全不同的部分。从结构的角度来看,将表单控件与其标签分开是不明智的,应该尽可能避免。

输入字段和文本区域

在这个简单的例子中,我们有两种类型的表单控件元素:input 和 textarea。文本区域用于键入多行文本,就像在注释字段中一样。cols 和 rows 属性可用于设置文本区域的默认大小,主要用于指示预期内容的大致长度。稍后我们将自由地用 CSS 进一步样式化文本区域。

<textarea name="comment-text" id="comment-text" cols="20" rows="10"></textarea>

input 元素是一个更加通用的表单控件。默认情况下,它呈现为单行文本输入,但是 type 属性可以将其更改为各种不同的窗体控件。设置 type="password "会创建一个值模糊的输入,而 type="checkbox "会创建一个复选框。type 属性有许多不同的值,其中许多是在 HTML5 中添加的。有些主要是文本输入的变体,但在幕后有特殊的行为—例如,电子邮件、url 和搜索。一些类型创建了非常不同的界面控件,如复选框、单选、颜色、范围和文件。除了类型之外,还有一大堆用于输入的属性来声明预期的格式。

不同类型的表单输入及其属性对于表单的自动验证非常有用。我们将在这一章的后面简要介绍一下,但是现在,我们将讨论另一个大的好处。在带有屏幕键盘的设备上,更改类型会触发软件键盘更改其布局。如果我们添加正确类型的电子邮件字段和 URL 字段,智能手机和平板电脑上的键盘将自动调整,以便在我们聚焦每个字段时更容易键入正确的值(见图 9-10 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-10。当输入具有 type="email "属性时,软件键盘会显示更适合键入电子邮件地址的布局

因为 type 属性的缺省值是 text,所以不支持 HTML5 的旧浏览器将忽略这些新类型,并退回到普通输入。这使得选择新的输入类型对我们来说是一个非常有用的改进。

将字段集放在一起

使用到目前为止我们已经看到的结构元素,我们可以通过标记第一个字段集的内容来开始布局表单。未样式化的字段集如图 9-11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-11。无样式字段集

在表单内部,我们用一个 div 包装了 fieldset 元素,原因稍后将会清楚。标签和输入的每个组合也用 p 元素包装。过去,输入元素不允许作为表单元素的直接子元素。HTML5 中不再是这种情况,但是标准仍然建议您用像 p 这样的块元素包装标签和表单控件,因为它们在语义上表示表单中不同的内容“短语”。

我们还为每个段落添加了一个 field 的类名,以便在以后想要将它们与表单中的其他类型的段落分开时有一个特定的样式挂钩。此外,我们通过给包含文本输入组件的字段起一个类名称 field-text 来分离它们。

<form id="comments_form" action="/comments/" method="post">
  <div class="fieldset-wrapper">
    <fieldset>
      <legend>Your Contact Details</legend>
      <p class="field field-text">
        <label for="comment-author">Name:</label>
        <input name="comment-author" id="comment-author" type="text" />
      </p>
      <p class="field field-text">
        <label for="comment-email">Email Address:</label>
        <input name="comment-email" id="comment-email" type="email" />
      </p>
      <p class="field field-text">
        <label for="comment-url">Web Address:</label>
        <input name="comment-url" id="comment-url" type="url" />
      </p>
    </fieldset>
  </div>
</form>

如果您希望更改字段集和图例元素的默认外观,最好的办法是不要设计实际字段集元素本身的样式,而是尽可能多地删除默认样式,然后在字段集周围添加一个包装元素。您的样式将被添加到包装元素中。

要取消字段集的样式,我们将为其指定以下规则:

fieldset {
  border: 0;
  padding: 0.01px 0 0 0;
  margin: 0;
  min-width: 0;
  display: table-cell;
}

我们删除了默认的边界和空白。我们还将填充设置为 0——顶部填充除外,它被设置为一个很小的值(0.01 像素)。这是为了应对一些基于 WebKit 的浏览器中的怪异行为,在这些浏览器中,图例之后元素上的任何边距都被转移到 fieldset 元素的顶部。给字段集一点点填充顶部可以阻止这个错误。

下一个奇怪之处是:一些浏览器(基于 WebKit 和 Blink 的)对字段集元素有一个默认的最小宽度,我们会覆盖它——如果没有,字段集有时会以最小的尺寸突出视口,产生水平滚动条。Firefox 也有字段集元素的最小宽度,但这是硬编码的,覆盖最小宽度没有帮助。解决方法是将显示模式改为表格单元。这与 IE 混淆了,所以我们需要使用特定于 Mozilla 的非标准规则块,只针对基于 Mozilla 的浏览器:

@-moz-document url-prefix() {
  fieldset {
    display: table-cell;
  }
}

@-moz-document 规则允许基于 Mozilla 浏览器的用户在其用户样式表中覆盖特定站点的样式,但它也适用于作者样式。通常,您会在 url-prefix()函数中放入一个特定的 url,但是让它为空意味着不管 URL 是什么,它都会工作。不可否认,这是一个丑陋的黑客,但它代表了移除我们的字段集的默认样式的最后一块拼图。现在我们可以专注于包装器的样式。

我们将给包装一个背景,一些空白和填充,和一个微妙的阴影。不支持 box-shadow 属性的旧浏览器会得到一个边框,然后使用:root 伪类作为选择器的前缀来删除它。这只是指 HTML 元素(作为文档的根元素),但 IE8 和其他旧浏览器不理解这个选择器,所以它们将获得边框。

.fieldset-wrapper {
  padding: 1em;
  margin-bottom: 1em;
  border: 1px solid #eee;
  background-color: #fff;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
}
:root .fieldset-wrapper {
  border: 0;
}

至于图例,我们将删除默认填充,并在底部添加一些额外的内容,以增加它和表单字段之间的空间。遗憾的是,边距对图例元素的影响不一致,因此我们将避免这些影响。最后,我们将把它的显示模式改为表格。这种方法允许它在必要时在 IE 中包装成多行,否则这是不可能的。

legend {
  padding: 0 0 .5em 0;
  font-weight: bold;
  color: #777;
  display: table;
}

此时,字段集本身看起来不错,如图 9-12 所示,我们可以将注意力转向字段。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-12。字段集现在失去了图例的双边框和奇怪的定位,并获得了背景和阴影
设置文本输入字段的样式

接下来,我们将添加一个规则,使表单控件从文档的其余部分继承字体属性。这将覆盖浏览器的默认设置:例如,输入字段中的字体大小被设置为小于文档中正常文本的大小。

input,
textarea {
  font: inherit;
}

定位标签使其垂直出现在表单元素的上方非常简单。默认情况下,标签是内联元素。将它的 display 属性设置为 block 将导致它生成自己的块框,将输入元素强制放到下面的行上。

文本输入框的默认宽度因浏览器而异,但是我们可以用 CSS 来控制它。为了创建一个灵活的输入字段,我们将在默认情况下以百分比来设置宽度,但是在 ems 中设置字段包装器的最大宽度,这样它就不会变得太宽。这将适用于大多数屏幕尺寸。在计算 100%的含义时,我们还需要更改 box-sizing 属性以考虑边框和填充。

.field-text {
  max-width: 20em;
}
.field-text label {
  cursor: pointer;
}
.field-text label,
.field-text input {
  width: 100%;
  box-sizing: border-box;
}

标签的 cursor 属性设置为 pointer,使基于鼠标的用户更清楚这是一个可点击的元素。标签也包含在上面设置宽度的规则中,因此它们与输入具有相同的宽度。

最后,我们将稍微调整文本输入的样式。我们将为它们设置微妙的圆角,设置边框颜色,并添加一些填充:

.field-text input {
  padding: .375em .3125em .3125em;
  border: 1px solid #ccc;
  border-radius: .25em;
  -webkit-appearance: none;
}

设置 border 属性通常会删除呈现文本输入时可能显示的任何特定于操作系统的边框外观和嵌入阴影。一些基于 WebKit 的浏览器(如 iOS 上的 Safari)仍然显示嵌入阴影,因此为了消除这种情况,我们将 proprietary -webkit-appearance 属性设置为 none。

注意

没有标准化的外观属性,但是-webkit-appearance(基于 webkit 和 Blink 的浏览器)和-moz-appearance (Firefox)都允许您覆盖特定于操作系统的控件的一些呈现细节。通常,你最好避开这些,但是它们对于移除特定于浏览器的输入元素样式很有用。

处理焦点状态

更改输入元素的边框后,我们还需要注意元素的焦点状态。当输入元素被聚焦时,大多数浏览器会在输入元素周围显示某种形式的轮廓或光晕。此标记帮助用户区分哪个字段是焦点,并且可以通过重写 outline 属性或 border 属性来移除,具体取决于浏览器。一旦我们影响了这些属性中的,我们需要确保我们没有无意中让键盘用户无法访问表单。

这意味着为了跨浏览器的兼容性,我们必须自己关注焦点状态。我们将添加一个不同的边框颜色:焦点,以及一个微妙的蓝色发光使用框阴影(见图 9-13 )。这样,我们还可以在聚焦时将 outline 属性设置为 0,以避免在某些浏览器中出现聚焦状态的双重标记。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-13。一个聚焦的文本输入使用框阴影得到不同的边框颜色和一点光晕
.field-text input:focus {
  box-shadow: 0 0 .5em rgba(93, 162, 248, 0.5);
  border-color: #5da2f8;
  outline: 0;
}

在我们到目前为止创建的规则中,我们已经明确地将我们在这个表单中使用的基于文本的输入类型作为目标。字段文本选择器。这是为了避免为其他类型的输入部件(如复选框)设置不必要的规则。我们本来可以用一个属性选择器列表来代替,但是因为 type 属性有许多可能的值,所以父元素上的一个实用程序类名称会使代码更加简洁。

添加剩余的字段集

到目前为止,我们创建的规则同样适用于其他表单元素,如 textareas:

<div class="fieldset-wrapper">
  <fieldset>
    <legend>Comments</legend>
    <p class="field field-text">
      <label for="comment-text">Message:</label>
      <textarea name="comment-text" id="comment-text" cols="20" rows="10"></textarea>
    </p>
  </fieldset>
</div>

要调整 textarea 元素的外观,我们可以简单地将它添加到任何规则的选择器列表中,在这里我们为文本输入和标签设置属性,并获得相同的行为:

.field-text label,
.field-text input,
**.field-text textarea {** 
**/*...*/** 
**}** 

Textareas 将获得基于 rows 属性的默认高度,但是我们当然可以用 height 来覆盖它。当用户输入比可见空间更长的文本时,文本区将溢出并接收滚动条。

许多浏览器还会让用户调整文本区域的大小,这样他们就可以看到输入的整个文本。有些浏览器允许文本区域调整宽度和高度,有些只允许调整高度。我们实际上可以在 CSS 中使用 resize 属性来明确这一点。它可以设定为任何关键字“垂直”、“水平”、“无”或两者兼有,但此处显示为设定为“垂直”:

textarea {
  height: 10em;
  resize: vertical;
}
添加单选按钮

对于表单的最后一部分,我们添加了一个单选按钮控件,使用户只能从两个选项中选择一个。这些由类型设置为 radio 的输入元素表示。这些元素的标签通常在右边,而不是在上面(见图 9-14 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-14。我们的单选按钮,标签文本在右边而不是上面

只能选择其中一个输入的效果是通过使这两个输入的 name 属性相等来实现的(尽管 id 属性仍然可以不同):

<div class="fieldset-wrapper">
  <fieldset>
    <legend>Remember Me</legend>
    <p class="field">
      <label><input **name="comment-remember"** type="radio" value="yes" />Yes</label>
    </p>
    <p class="field">
      <label><input **name="comment-remember"** type="radio" value="no" checked="checked" />No</label>
    </p>
  </fieldset>
</div>

注意,在这种情况下,我们选择将输入嵌套在标签中,而不是将其与输入元素上的 for 属性和 id 相关联。这意味着标签上的 display: block 声明不会将它放在与单选按钮不同的行上。

我们要做的最后一件事是给单选按钮添加一点右边距,以在标签之间提供一些间距:

input[type="radio"] {
  margin-right: .75em;
}
小跟班

在我们的表格完成之前,我们还有一件事要添加。用户需要一个按钮来提交表单,这样服务器就可以处理它。

用 HTML 创建按钮有两种方法。首先,输入元素的类型设置为 button、reset 或 submit:

<input type="submit" value="Post comment" />

然后是按钮元素,它可以具有相同的类型属性值:

<button type="submit">Post comment</button>

当在表单外使用时,按钮类型可用于 JavaScript 启动的操作,而不是实际将表单提交给服务器。重置类型(目前不常用)将表单重置为初始值。最后,如果按钮在表单元素中,submit 值将表单数据发送到表单的 action 属性中指定的 URL。当缺少 type 属性时,它是默认值。

按钮的这两个元素工作方式相同,最初看起来也一样。我们建议您为按钮使用 button 元素,因为您可以在其中放置其他元素(如 spans 或 images)来帮助设计样式。

按钮在每个平台上都有特定的默认外观(见图 9-15 ),复选框、单选按钮和其他表单控件也是如此。因为按钮是用户界面中无处不在的一部分,所以你很有可能想要根据你正在构建的站点的特定外观来设计它们。幸运的是,它们是用 CSS 样式化的更容易的表单组件之一。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-15。来自 OS X 的 Chrome、Windows 7 的 IE10、Windows 7 的 Firefox 和 Windows 10 的 Microsoft Edge 的无样式按钮元素

我们将使用渐变和方框阴影为我们的按钮添加一个非常微妙的 3D 外观的边缘(见图 9-16 )。与 input 元素一样,修改 border 属性会关闭特定于操作系统的样式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-16。我们的样式按钮
button {
  cursor: pointer;
  border: 0;
  padding: .5em 1em;
  color: #fff;
  border-radius: .25em;
  font-size: 1em;
  background-color: #173b6d;
  background-image: linear-gradient(to bottom, #1a4a8e, #173b6d);
  box-shadow: 0 .25em 0 rgba(23, 59, 109, 0.3), inset 0 1px 0 rgba(0, 0, 0, 0.3);
}

按钮上的伪 3D 边缘是用框阴影而不是边框属性创建的。这允许我们保持按钮的尺寸不变,因为阴影不会影响盒子模型。阴影也会自动跟随按钮的圆角。注意,我们使用了两个阴影。一个是创建边缘的外部阴影,一个是插入,在按钮顶部添加一个微妙的 1 像素的颜色偏移。

我们还为按钮的焦点状态添加了一条规则(见图 9-17 ),这里的背景变浅了一点,并添加了第三个阴影,产生了与文本输入相同的轻微“发光轮廓”效果。(在第十章的中,当我们探索变换和转换的时候,我们将会重温按钮被按下的状态。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-17。按钮的正常状态(左)和聚焦状态(右)
button:focus {
  background-color: #2158a9;
  background-image: linear-gradient(to bottom, #2063c0, #1d4d90);
  box-shadow: 0 .25em 0 rgba(23, 59, 109, 0.3),
              inset 0 1px 0 rgba(0, 0, 0, 0.3);
}

不支持圆角、渐变和方框阴影的浏览器在这两种状态下都会有一个看起来很平的按钮,但是仍然可以很好地使用。

清除表单反馈和帮助文本

糟糕的反馈和错误信息一直被认为是网络上最糟糕和最常见的设计问题。当设计表单时,确保你不只是让表单控件看起来漂亮,还要注意帮助和错误信息的样式。

您可以使用占位符属性将预期输入的示例放入输入字段(参见图 9-18 )。浏览器将显示该文本,直到您聚焦该字段或开始书写。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-18。使用占位符属性的输入示例
<input **placeholder="http://example.com"** name="comment-url" id="comment-url" type="url" />

我们可以对占位符属性进行一些有限的样式设置,例如,将其设置为斜体。占位符没有标准的选择器,但是不同的浏览器提供了不同的前缀伪元素,您可以对它们进行样式化。因为每一个都只能被各自的浏览器引擎识别,所以它们不能合并成一个规则。当浏览器看到一个无法识别的选择器时,它会将规则作为一个整体丢弃,所以我们需要重复一下:

::-webkit-input-placeholder {
  font-style: italic;
}
:-ms-input-placeholder {  
  font-style: italic;
}
::-moz-placeholder {
  font-style: italic;  
}

占位符是用来举例输入的,所以不能用作标签。毕竟,占位符会随着用户与表单的交互而消失,所以如果用户失去焦点(没有双关语!)在一段时间内,他们需要能够让任何指令仍然存在。

如果标签不够,可以在表单控件旁边添加帮助文本。因为我们想节省空间并使表单更整洁,所以我们将使用同级选择器仅在输入字段被聚焦时显示额外的帮助(见图 9-19 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-19。当字段被聚焦时显示额外的帮助文本

我们希望在视觉上隐藏文本,但不一定是为了屏幕阅读器,即使字段没有被聚焦。使用 clip 属性、绝对定位和 overflow: hidden 的组合可以达到这个目的。

这种特定的属性组合是为了避免旧浏览器中的各种错误。在 Jonathan Snook 的博客(Snook . ca/archives/html _ and _ CSS/hiding-content-for-accessibility)中可以找到关于这种技术的更深入的讨论。然后,当帮助文本位于焦点输入旁边时,使用同级选择器覆盖这些属性:

.form-help {
  display: block;
  /* hide the help by default,
     without hiding it from screen readers */
**position: absolute;** 
**overflow: hidden;** 
**width: 1px;** 
**height: 1px;** 
**clip: rect(0 0 0 0);** 
}

input:focus + .form-help {
  padding: .5em;
  margin-top: .5em;
  border: 1px solid #2a80fa;
  border-radius: .25em;
  font-style: italic;
  color: #737373;
  background-color: #fff;
  /* override the "hiding" properties: */
**position: static;** 
**width: auto;** 
**height: auto;** 
**crop: none;** 
}
易接近的隐藏技术

帮助文本示例中的技术使用 CSS 来隐藏可视内容,同时保持屏幕阅读器可以访问它。使用其他技术,如 display: none 或 visibility: hidden,可以让屏幕阅读器完全跳过文本。

在设计表单时,视觉设计中省略一个标签或者多个字段使用一个标签是很常见的模式。例如,您可能将日期部分分成三个字段,分别表示年、月和日,但只有一个标签显示“出生日期”

在这种情况下,使用可访问的隐藏技术允许您为任何字段添加标签,而无需在页面上实际显示它。当然,这种技术可以用于任何有助于页面语义结构的元素,但对于视觉用户来说可能是不必要的。

这是“助手类”的理想选择,只要出现这种情况,就可以应用到标记中。HTML5 样板项目(html5boilerplate.com/)对类名使用了这种技术。例如,视觉隐藏。

我们的帮助文本的标记非常简单,但是增加了一些语义丰富性,以确保帮助是可访问的:

<input placeholder="http://example.com" name="comment-url" **aria-described-by="comment-url-help"** id="comment-url" type="url"  />
<span **id="comment-url-help" role="tooltip"** class="form-help">Fill in your URL if you have one. Make sure to include the "http://"-part.</span>

input 元素上的 aria-describedby 属性应该指向帮助文本的 id。这使得屏幕阅读器将帮助文本与字段相关联,并且当字段被聚焦时,他们中的许多人除了阅读标签之外还会阅读帮助文本。设置为 tooltip 的角色属性进一步向屏幕阅读器阐明了这是在用户与表单域交互时显示的文本。

如果您在服务器上或者在浏览器中使用 JavaScript 进行表单验证,HTML 中的任何错误消息都可以用类似的方式标记,使用 aria-describedby 将消息与表单控件相关联。

支持 HTML5 的现代浏览器也有内置的表单验证,以及一系列帮助客户端验证的 CSS 伪类。

HTML5 表单验证和 CSS

一旦您使用 HTML5 表单的新属性,浏览器将尝试帮助您验证表单字段的值。例如,当您使用类型设置为电子邮件的输入,并填写无效的内容并尝试提交表单时,浏览器将显示一条错误消息(参见图 9-20 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-20。Mozilla Firefox 中的验证消息

支持 HTML5 验证的浏览器也为我们提供了许多对应于表单字段中各种状态的伪类。例如,我们可以使用下面的代码用红色边框和红光突出显示无效的文本输入字段:

.field-text :invalid {
  border-color: #e72633;
  box-shadow: 0 0 .5em rgba(229, 43, 37, 0.5);
}

我们已经在第二章中看到了:required,:optional,:valid 和:invalid 伪类。它们还有几个,对应于数字输入、滑块等的各种状态。基于这些伪类设计输入字段的样式没有问题,但是实际的错误消息呢?

可悲的是,这些是 CSS 无法触及的界面元素的另一个例子。基于 WebKit 的浏览器提供了一些有限的可能性来使用特定于浏览器的伪元素来样式化错误消息,例如::-webkit-validation-bubble,但是除此之外,还没有其他方法来样式化它们的外观。

如果您需要对这些错误消息进行更多的控制,有许多 JavaScript 插件可以与浏览器触发的表单事件挂钩。它们覆盖内置的验证,通常为您提供为错误消息生成元素(和设置文本)的方法,并为旧浏览器提供验证支持。例如,参见 Webshim 项目的表单插件(afarkas.github.io/webshim/demos/)。

高级表单样式

到目前为止,我们将表单样式保持在非常合理的最低限度。理由很充分:表单很少是实验的地方。想要注册个人资料或为产品付费的人可能会喜欢清晰,而不是为了清晰而与众不同。这并不意味着更有创造性的 CSS 技术在表单设计中没有一席之地。在这一节中,我们将展示一些技巧来解决细节问题。

用于表单布局的现代 CSS

默认情况下,大多数表单元素显示为内嵌块,因此沿文本方向排列。我们在前面的示例中使用了块显示模式,使标签和输入字段在页面上堆叠显示。

当我们想要更高级的表单布局时,一些新的布局机制真的大放异彩。Flexbox 是专门针对按钮的行或列或其他界面元素而创建的,它们之间的空间需要以一种巧妙的方式进行划分。这是表单的常见情况,所以让我们看一个使用 flexbox 的例子。

基于我们在前一个简单示例中看到的样式,让我们创建一个稍微复杂一点的表单,在这里我们收集一些关于求职者的信息。我们的目标是收集申请人的姓名、电子邮件地址和 Twitter 账号,以及申请人已经掌握的编码语言列表(见图 9-21 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-21。我们的申请表

对于较大的视口,表单的顶部从堆叠版本(标签堆叠在字段上)切换到标签与字段显示在同一行的版本。Twitter handle 字段前面还有一些指导文本,表明只需要填写“@”后面的部分。让我们从内联字段开始。

我们将使用 flexbox 来控制字段布局。为了检测它,我们将使用在第六章中看到的 Modernizr 库。简单回顾一下,Modernizr 可以通过 JavaScript 检测 CSS 特性,并将每个受支持特性的类名添加到页面的 html 元素中。您可以创建一个自定义脚本,只包含您在 https://modernizr.com 的需要的检测。在这种情况下,flexbox 检测将 flexbox 类添加到 html 元素中。

我们现在可以开始使用。flexbox 类作为选择器前缀,并且确信只有支持它的浏览器才能看到它。

首先,我们只想在视窗大到足以处理它时服务于内联布局。大约 560 像素看起来差不多,也就是 35em:

@media only screen and (min-width: 35em) {
  /* the rest of the code snippets go here */
}

接下来,我们的文本输入字段需要在较大的视口中变成一个 flex 容器,其中的项目水平排列(这是默认设置)。它们还需要具有更大的最大宽度。

.flexbox .field-text {
  display: flex;
  max-width: 28em;
}

我们希望标签都具有相同的宽度(大约 8em 似乎就可以了),既不收缩也不增长,也就是说,flex-grow 和 flex-shrink 设置为 0,flex-basis 为 8em:

.flexbox .field-text label {
  flex: 0 0 8em;
}

至于标签文本,我们希望它垂直居中。我们可以用行高来做这件事,但是我们会把它和输入元素的高度联系起来。Flexbox 实际上也可以帮助我们做到这一点,不需要具体的测量。

为了达到这种效果,我们需要将标签本身声明为内容居中的 flex 容器。因为 label 元素没有可以居中的子元素,所以我们依赖于这样一个事实,即 flex 容器中的任何文本内容都成为一个匿名的 flex 项。然后,我们可以告诉容器将其所有项目垂直居中。

.flexbox .field-text label {
  flex: 0 0 8em;
**display: flex;** 
**align-items: center;** 
}

这为我们提供了更大视窗的最终视场布局,如图 9-22 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-22。用于较大视口的内嵌标签/字段放置

至于输入元素的宽度,flexbox 会自动算出来。它们已经设置为宽度:之前的 100%,由于默认的伸缩值为 0 1 auto,因此将会缩小以便为固定宽度的标签腾出空间。读出该值意味着“根据 width 属性(auto)确定宽度,不要超过该值,但可以随意缩小以腾出空间。”

带 Flexbox 前缀的输入字段

当谈到前置文本时,这是 flexbox 真正擅长的情况。我们有一些限制,使用任何其他布局技术都很难灵活解决:

  • 输入和前置文本组件需要具有相同的高度。

  • 前置元素的宽度需要保持灵活,这取决于里面的文本。

  • 然后需要调整输入的宽度,以便前置文本和输入字段的组合与其他文本字段的宽度相同。

为了定位这些组件,我们将把整个东西包装在一个 span 中,并应用一些通用的类名。我们还将添加相关属性,以使前置文本的目的易于理解。以下是该字段的完整标记:

<p class="field field-text">
  <label for="applicant-twitter">Twitter handle:</label>
**<span class="field-prefixed">** 
**<span class="field-prefix" id="applicant-twitter-prefix" aria-label="You can omit the @">@</span>** 
**<input aria-describedby="applicant-twitter-prefix" name="applicant-twitter" id="applicant-twitter" type="text" />** 
**</span>** 
</p>

aria-label 属性在这里为 prefix 元素提供了一个屏幕阅读器可访问的名称,解释了带前缀的文本的用途。

对于样式,我们将首先为不支持 flexbox 的浏览器创建一个后备样式。我们将保持简单,只提供一个包含前置文本的内嵌块框,并使它旁边的输入足够短,不会在小屏幕上出现在不同的行上(见图 9-23 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-23。我们的基线只是一个样式化的内联块框中的前置文本
.field-prefix {
  display: inline-block;
  /* border and color etc omitted for brevity. */
  border-radius: .25em;
}
.field-prefixed input {
  max-width: 12em;
}

我们还必须补充设置输入宽度的规则,方法是将我们预先添加的字段类名添加到选择器中:

.field-text label,
.field-text input,
**.field-prefixed,** 
.field-text textarea {
  /* ... */
}

最后,我们将应用 flexbox 的魔力,使用。flexbox 类名来限定我们的规则。我们会赶上的。字段前缀包装到 flex 容器中,并将前缀元素的内容垂直居中。就像前面内联字段示例中的标签一样,我们创建一个嵌套的 flex 容器,并在里面垂直对齐匿名项。我们还调整了边框,使其只在物品的外角变圆。

.flexbox .field-prefixed {
  display: flex;
}
.flexbox .field-prefix {
  border-right: 0;
  border-radius: .25em 0 0 .25em;
  display: flex;
  align-items: center;
}

输入需要重新应用它的最大宽度。除此之外,它会自动填满剩余的空间。

.flexbox .field-prefixed input {
  max-width: 100%;
  border-radius: 0 .25em .25em 0;
}

图 9-24 显示了结果。这些样式为将文本(和其他控件)添加到文本输入字段提供了一个灵活且可重用的基础,同时使它们与其他输入保持相同的宽度。它可以很容易地扩展为在字段后追加*。*

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-24。完成的前缀字段
对复选框集合使用多列布局

与内联字段放置可以节省垂直空间一样,我们可以通过将复选框集合放置到列中来节省空间。多列布局模块是一个很好的选择,在不支持的情况下,它会回到单列布局。

我们的标记非常简单:一个带有复选框类名的无序列表,在每个列表项中包含单个复选框及其相关标签。

<ul class="checkboxes">
  <li>
    <input type="checkbox" name="lang-as" id="lang-as">
    <label for="lang-as">ActionScript</label>
  </li>
  <!-- ...and so on-->
</ul>

我们可以像处理单选按钮一样将复选框嵌套在标签中,但是在下一个示例中,这里的顺序实际上将用于样式目的。

要让复选框按列排列,我们可以简单地告诉浏览器每列的最小宽度。考虑到较长的标签,10 个左右的 ems 似乎是合理的。除此之外,我们将删除列表样式,并调整边距和填充。图 9-25 显示了复选框列。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-25。中等大小的视窗中自动生成的四列中的复选框
.checkboxes {
  list-style: none;
  padding: 0;
  column-width: 10em;
}
.checkboxes li {
  margin-bottom: .5em;
}
设置不稳定的样式:仿自定义复选框

我们已经看到,按钮和文本输入可以通过移除默认样式(如边框)来驯服。但是按钮大多只是一个平面,里面有一些文本,其他表单组件更复杂。例如,复选框由小方框和其中可能的勾号组成。例如,对复选框应用填充是什么意思?它是在盒子的图形里面还是外面?我们对复选框应用的任何大小调整会影响复选标记吗?

避开这些问题,我们可以选择用图形完全替换复选框。这是通过巧妙使用 label 元素和表单状态的伪类实现的。

由于标记的顺序,结合兄弟选择器和:checked 伪类,我们可以根据复选框的状态为复选框和标签的外观生成规则。

我们还需要在浏览器支持方面划清界限。不理解选择器的旧浏览器,比如:checked 需要退回到非样式化的本地复选框。为了实现这一点,我们将重用:root 选择器技巧,它会导致 IE8 等旧浏览器跳过整个规则:

:root input[type="checkbox"] + label {
  /* unchecked checkbox label */
}
:root input[type="checkbox"]:checked + label {
  /* checked checkbox label */
}

接下来,我们需要使复选框本身不可见,但仍然可以访问和聚焦。我们将使用我们自己创建的复选框的图像作为标签的背景图像。图 9-26 说明了思路。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-26。使用 CSS 隐藏复选框本身,label 元素有一个显示假复选框的背景图像

使用鼠标或触摸屏的人可以点击标签,这将触发复选框改变状态,从而更新样式。键盘用户仍然可以聚焦复选框并与之交互,状态同样会反映在标签的样式中。

这项技术有两个关键部分。首先,标签需要紧跟在标记中的 input 元素之后,并具有关联两者的适当的 for 属性。第二,标签需要隐藏但仍可访问。最后一个问题是为它提供与前面示例中隐藏的帮助消息相同的样式集合:

:root input[type="checkbox"] {
  position: absolute;
  overflow: hidden;
  width: 1px;
  height: 1px;
  clip: rect(0 0 0 0);
}

现在我们需要为复选框的各种状态的图像提供规则,包括键盘访问的焦点状态。我们总共需要四个图像:未选中的、选中的、带焦点的未选中的和带焦点的选中的。图 9-27 显示了最终结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-27。我们的复选框样式的结果:我们的复选框现在在所有现代浏览器中遵循页面的整体颜色主题

我们使用 Modernizr 来检测对 SVG 的支持,因此我们的规则中添加了一个 svgasimg 类:

注意

Modernizr 测试实际上检测了对 SVG 中的元素的支持,但是它与对背景图像的 SVG 支持重叠得很好,否则是检测不到的,还有用于 img 元素源的 SVG 文件(因此有了类名)。

:root.svgasimg input[type="checkbox"] + label {
  background: url(img/checkbox-unchecked.svg) .125em 50% no-repeat;
}
:root.svgasimg input[type="checkbox"]:checked + label {
  background-image: url(img/checkbox-checked.svg);
}
:root.svgasimg input[type="checkbox"]:focus + label {
  background-image: url(img/checkbox-unchecked-focus.svg);
}
:root.svgasimg input[type="checkbox"]:focus:checked + label {
  background-image: url(img/checkbox-checked-focus.svg);
}

在最后的示例文件中,我们还包含了一些标签填充和文本样式的规则。我们还对(微小的)SVG 文件进行了 URL 编码,并将它们作为数据 URIs 内嵌在 CSS 文件中,这有助于减少请求数量。

遗憾的是,有些浏览器支持我们用过的所有选择器,但不支持 SVG。为了解决这个问题,我们需要使用一个后备解决方案,如果 JavaScript 无法运行或者浏览器不支持 SVG 作为背景图像,我们就退回到 PNG 图像。我们在 CSS 中的 SVG 解决方案之前添加了基于 PNG 的解决方案:

:root input[type="checkbox"] + label {
  background-image: url(img/checkbox-unchecked.png);
}
:root input[type="checkbox"]:checked + label {
  background-image: url(img/checkbox-checked.png);
}
/* ...and so on. */

好处是我们现在在任何支持表单伪类的浏览器中都完全支持我们的自定义复选框。IE8(及更老版本)退回到普通的本地复选框。完全相同的技术也可以用于单选按钮,在样式方面,您可以随心所欲。

你也可以为复选框图形使用其他技术,包括动画显示选中和取消选中的动作,就像马诺埃拉·伊里奇(tympanus.net/Development/AnimatedCheckboxes/)的演示一样,如图 9-28 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-28。一个使用动画 SVG 图形来“铅笔”单选按钮选项的演示

缺点是我们引入了一个对 JavaScript 的小依赖,以增强我们的 checkbox 组件的全部潜力,但这是一个非常小的依赖,并且有一个非常好的后备。

关于自定义表单小部件的一句话

到目前为止,我们已经看到,我们可以成功地用 CSS 样式化输入字段和按钮。我们还可以使用 CSS 和图像替换技术来设计复选框和单选按钮的样式。select 元素是一个稍微复杂一点的表单控件,由下拉菜单本身、箭头指示器和选项列表组成。还有像 input 元素的文件上传和颜色选择器版本这样的东西,它们有更复杂的小部件来表示。

传统上,这些类型的小部件实际上不可能进行样式化,导致大量 JavaScript 驱动的解决方案使用常规的 div 和 spans 来伪造文件选择器或 select 元素的外观。虽然这些解决方案解决了能够对小部件进行样式化的问题,但它们通常会带来更难解决的新挑战。

这些挑战包括在移动设备上不中断,使用与原生版本相同的键盘控制,以及跨不同设备和浏览器运行。例如,试图在一个选择元素中伪造选项,然后对其进行样式化是一件非常危险的事情,因为控件在移动设备上看起来完全不同(见图 9-29 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-29。iOS 上的选择元素根本不显示选择下方的选项,而是在屏幕底部触发一个旋转器类型的小部件

当决定一个设计时,你可能要再三考虑自定义这些类型的控件,以及让它们匹配页面的主题是否值得潜在的麻烦。

也就是说,也有很多开发人员努力尝试使用 JavaScript 以深思熟虑的方式解决问题,所以使用第三方库的选项是存在的。这些库大多依赖于 jQuery 这样的通用 DOM 操作库,因为页面中元素的创建和处理对于这类小部件来说很快就变得不简单了。

您可能想要签出以下任何一个库:

  • Filament Group 为 jQuery 发布了一个简单的选择菜单插件,它的工作方式类似于前面描述的复选框技术,但是增加了一些 JS 技巧。它提供了一种快速的方式来设计选择元素本身的样式,但不是选项列表:github.com/filamentgroup/select。Filament Group 也有一个小插件,使用类似的方法进行文件输入。

  • chosen(harvesthq.github.io/chosen/)和 Select 2(select2.github.io/)是两个比较流行的 jQuery 插件,用于 select 元素的高级增强。选项包括设置占位符和选项的样式、搜索或过滤,以及更好的多选 UI。这两个库在最近的版本中都在可访问性方面做了改进,但是你应该知道它们可能仍然有问题。

开发人员 Todd Parker 做出了巨大的努力,为最简单的选择元素下拉按钮的样式找到了这样一个纯粹的 CSS 解决方案。你可以在 http://filamentgroup.github.io/select-css/demo/看到他的解决方案。在撰写本文时,它更多的是一种概念验证,而不是一种已完成的技术,但是它设法在大多数浏览器中对 select 元素(没有选项)进行样式化,不使用 JavaScript,只使用一个包装器元素进行样式化。旧的浏览器被聪明的黑客过滤掉了,所以它们得到了无样式的默认选择。

对于表单,无论您使用自定义样式还是高级小部件,都要确保在现场使用时,它们能像本地元素一样工作。

摘要

在这一章中,我们已经学习了设计表单和表格的样式。它们是一些更复杂的 HTML 元素的集合,但是对于帮助用户与网页交互和理解复杂的数据来说通常是至关重要的。

我们已经了解了如何设计数据表的样式,以及一种简单的方法来使它们具有响应性。

我们创建了一个简单的表单,并学习了如何设计字段集、标签、文本输入和按钮的样式。我们还了解了如何使用现代 CSS 布局技术来更有效地利用表单中的空间,以及如何解决复选框和单选按钮等表单组件的样式问题。

在下一章中,我们将把交互性提升到另一个层次,并向您展示如何使用变换、过渡和动画使您的网页变得生动。

十、让它动起来:变换、过渡和动画

这一章是关于移动物体的——或者通过空间,变换,或者通过时间,使用动画和过渡。通常,这两个系列的属性一起工作。

变换与移动具有定位或其他布局属性的事物是不同的概念。事实上,变换一个对象根本不会影响页面的布局。您可以旋转,倾斜,平移和缩放元素,甚至在三维!

动画元素可以用 CSS 动画属性来完成。过渡是动画的一种简化形式。当你只有一个开关状态时(比如悬停在一个元素上),转换是用来自动完成这个过程的。

综上所述,这些属性为您的页面注入了活力。作为额外的奖励,他们也有非常好的表现。

在本章中,我们将讨论以下内容:

  • 二维变换:平移、缩放、旋转和倾斜

  • 简单和高级过渡效果

  • 你能做什么,不能做什么

  • 关键帧动画和动画属性

  • 三维转换和透视

这一切是如何结合在一起的

CSS 变换允许我们在空间中移动事物,而 CSS 过渡和 CSS 关键帧动画控制元素如何随时间变化。

即使这两个方面有些不相关,变换、过渡和关键帧动画通常在概念上被混为一谈。这是因为它们经常被用来相互补充。当制作动画时,你每秒钟要改变它的外观 60 次。转换允许您以浏览器可以非常有效地计算的方式描述外观的某些类型的变化。

转场和关键帧动画允许您以一种智能的方式制作这些变化的动画。因此,这些功能是相辅相成的。最终结果让我们有能力做一些事情,就像谷歌创造的动画 3D 弹出书(见图 10-1 )展示其产品的创造性使用(【http://creativeguidebook.appspot.com/】??)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-1。谷歌制作了一个动画 3D 立体书来展示其产品的创造性使用

因为这一章中的例子确实有很多移动的部分,所以很难在一本书的页面上描述它们。我们强烈建议您在阅读时在浏览器中尝试这些示例,以了解发生了什么。很多时候,JavaScript 用于交互性——我们不会深入研究脚本如何工作的细节,但是示例也包括 JS 文件供您探索。

关于浏览器支持的说明

变换、过渡和关键帧动画的规范仍在制定中。尽管如此,大多数这些功能在常用的浏览器上都得到了很好的支持,明显的例外是 Internet Explorer 8 和 Opera Mini。IE9 仅支持使用-ms 前缀的 2D 变换子集,不支持关键帧动画或过渡。变换、过渡和关键帧动画都需要-webkit-前缀才能在各种版本的基于 webkit 和 Blink 的浏览器中工作。只有当您需要覆盖旧版本的 Firefox 时,才需要-moz-前缀。

2D 变换

CSS 转换允许您通过平移、旋转、倾斜或缩放来改变页面上元素的呈现方式。此外,您可以在组合中添加第三个维度!在这一节,我们将从 2D 变换开始,稍后再看 3D。图 10-2 给出了 2D 变换的概述。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-2。不同类型的 2D 变换举例说明

用技术术语来说,当元素出现在页面上时,转换会改变元素的坐标系。看待变换的一种方式是将它们视为“扭曲场”任何属于已转换元素的渲染的像素都被捕捉到失真场中,并被传送到页面上的新位置或新大小。元素仍然保持在页面上原来的位置,但是元素的结果图像被转换。

假设页面上有一个 100×100 像素的元素,类名为 box。元素的位置会受到页边距、位置、页面流中其他元素的大小等因素的影响。不管它是如何结束的,我们都可以通过使用视口内的坐标来描述框的位置,例如,距离页面顶部 200 像素,距离页面左侧 200 像素。这是视口坐标系

在页面中,为元素保留了一个 100×100 像素的空间,就像它正常呈现的那样。现在,让我们设想将元素旋转 45 度进行变换:

.box {
**transform: rotate(45deg);** 
}

对一个元素应用变换会为该元素最初放置的空间创建一个所谓的局部坐标系。局部坐标系是变形场,它取代了元素的像素。

因为元素在页面上被表示为矩形,所以可能最容易想到的是在盒子的四个角上的四个点会发生什么。Firefox 开发人员工具在检查元素时有一个很好的可视化效果。在检查器的“规则”面板中,将鼠标悬停在变换规则上以查看结果变换(参见图 10-3 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-3。在 Firefox 开发工具中可视化 45 度转变。原始框和变换后的框与箭头一起显示,箭头显示角的改变位置。

页面仍然保留了它的 100×100 像素的间隙,该间隙曾经是框的位置,但是属于框的任何点现在都被扭曲场所转换。

除了影响元素在页面上的位置的其他属性之外,对元素应用转换时,理解这一技术背景非常重要。当我们对转换后的 div 应用 margin-top: 20px 时会发生什么?指向上方的角现在是否在距离原始位置顶部 20 个像素处结束?否:旋转属于盒子的任何东西的整个局部坐标系,包括边距,如图 10-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-4。旋转一个框包括旋转它的整个坐标系,因此上边距也会旋转

同样重要的是要注意,旋转后的外观不会干扰页面其余部分的布局,就像没有转换时一样。如果我们将盒子旋转整整 90 度,使上边距在视觉上向右突出,它不会推动可能位于盒子右侧的任何元素。

注意

页面上实际上有一样东西会受到变换元素的影响,那就是溢出。如果转换后的元素最终在任何具有导致滚动条的溢出属性的框的文本方向上突出,则转换后的元素可能会影响可滚动区。在从左到右的语言中,这意味着你可以使用向上或向左的平移来隐藏屏幕外的内容,但不能向下或向右。

变换原点

默认情况下,任何变换都基于元素边框的中心进行计算。负责控制的属性是 transform-origin。例如,我们可以围绕一个点旋转盒子,这个点距离盒子顶部 10 个像素,距离盒子左侧 10 个像素。

我们可以为 transform-origin 提供一到三个值,给出 x、y 和 z 轴的坐标。(z 轴用于 3D 变换;我们将在本章的后面回到这一点。)如果你只提供一个值,第二个被假定为关键字中心,就像你设置背景位置一样。第三个值不影响二维转换,所以我们现在可以放心地忽略它。

.box {
**transform-origin: 10px 10px;** 
  transform: rotate(45deg);
}

这给了我们旋转盒子时完全不同的结果,如图 10-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-5。围绕距离上边缘和左边缘 10 个像素的点旋转盒子
注意

如果将转换应用于 SVG 元素,那么转换的工作方式会有所不同。一个例子是 transform-origin 属性的默认值:它默认为左上角,而不是元素的中心。

翻译

翻译一个元素仅仅意味着把它移动到一个新的位置。您可以选择使用 translateX()和 translateY()函数沿单个轴平移,或者使用 translate()同时设置两者。

translate()函数的工作方式是向它提供一对表示 x 和 y 轴上平移量的位置。此数量可以是任何长度,如像素、ems 或百分比。值得注意的是,上下文中的百分比指的是元素本身的尺寸,不是包含的块。这允许你在不知道元素有多宽的情况下,将元素平移到其原始位置的右侧(见图 10-6 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-6。我们的盒子 100%向右平移
.box {
  /* equivalent to transform: translateX(100%); */
  transform: translate(100%, 0);
}

多重转换

一次应用多个变换是可能的。转换以空格分隔值列表的形式提供给 transform 属性,并按声明顺序应用。让我们看一个例子,我们既做平移又做旋转。

在这个例子中,我们将使用一个有序的规则列表来表示“搏击俱乐部”我们将对编号规则进行一些格式化,并将它们旋转到列表中每一项的下方(见图 10-7 )。我们希望列表编号从下到上读取,但定位在列表项的顶部。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-7。规则列表,带有向下旋转的编号项目

首先,我们的标记中需要一个有序列表。我们将从第三条规则开始,因为前两条规则神秘地丢失了:

<ol class="rules" start="3">
  <li>If someone says "stop", goes limp or taps out, the fight is over.</li>
  <li>Only two guys to a fight.</li>
  <li>One fight at a time.</li>
  <li>No shirts, no shoes.</li>
  <li>Fights will go on as long as they have to.</li>
  <li>If this is your first night at FIGHT CLUB, you HAVE to fight.</li>
</ol>

默认情况下,我们无法真正影响有序列表中数字的呈现。CSS 列表和计数器模块 3 级规范描述了一个::marker 伪元素来控制列表标记样式,但是在编写本文时还没有浏览器支持它。我们将发挥创造性,使用 CSS 中支持良好的计数器属性结合伪元素来解决这个问题。计数器允许您通过计算某些元素来生成数字,然后您可以将这些数字插入到页面中。

首先,我们删除了默认的列表样式(删除了数字),并添加了一个计数器重置规则。它告诉浏览器这个元素重置了一个名为 rulecount 的计数器的编号。这个名称是我们自己选择的任意标识符。名字后面的数字告诉计数器它应该有哪个初始值。

.rules {
  list-style: none;
  counter-reset: rulecount 2;
}

接下来,我们将告诉计数器在每次遇到列表中的列表项时增加 rulecount 值。这意味着第一个项目将被编号为 3 ,依此类推。

.rules li {
  counter-increment: rulecount;
}

最后,我们使用 content 属性将 rulecount 计数器中的数字作为伪元素注入到每个列表项的文本之前。我们将在数字前插入一个分段符号()。这给了我们在图 10-8 中看到的渲染内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-8。带有注入部分符号和编号的列表
.rules li:before {
  content: '§ ' counter(rulecount);
}

它看起来还不是很好,但是我们现在有一些东西可以抓取和设计,而不是默认的数字。接下来,我们将尝试沿嵌线的边垂直放置节编号,而不是与文本内联。

我们不希望列表数字在页面中占据任何空间,所以我们需要绝对地定位它们;这样做会自动将章节号放在项目的左上角。为了实现数字被旋转但固定在列表项顶部的效果,我们需要考虑如何平移和旋转它。我们将变换原点设置在右下方(100% 100%),将其向左平移 100%,向上平移 100%(记住百分比指的是变换元素的尺寸),然后逆时针旋转 90 度。图 10-9 显示了一步一步的转变。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-9。转换计数器的步骤,使其在列表项文本旁边从上到下运行
.rules li {
  counter-increment: rulecount;
**position: relative;** 
}
.rules li:before {
  content: '§ ' counter(rulecount);
**position: absolute;** 
**transform-origin: 100% 100%;** 
**transform: translate(-100%, -100%) rotate(-90deg);** 
}

转换函数的顺序在这里非常重要。如果我们从旋转伪元素开始,平移会相对于旋转的坐标发生,x 和 y 轴上的偏移会指向错误的方向 90 度!转换列表越来越多,因此您必须提前计划它们。

更改转换列表

当您声明一个转换列表时,您不能在事实之后添加它,而只能替换整个列表。例如,如果您有一个带平移的变换元素,并且您还想旋转它:hover,下面的将不会像预期的那样工作:

   .thing {
     transform: translate(0, 100px);
   }
   .thing:hover {
     /* CAUTION: this will remove the translation! */
     transform: rotate(45deg);
   }

相反,您必须重新声明整个列表,但是要附加循环:

   .thing:hover {
     /* preserves the initial translation, and then rotates. */
     transform: translate(0, 100px) rotate(45deg);
   }

在完成的示例中,我们已经向列表项添加了一个灰色的左边界,区域编号位于它的顶部。在这里使用边框的好处是,如果规则超过几行,它会自动延伸到列表项的高度。我们还添加了一些排版规则,并稍微调整了一下填充。例如,生成的部分编号在顶部有一点填充。但是请记住,这实际上意味着填充-右,因为它现在是旋转的!(参见图 10-10 。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-10。使用边框可以让我们在换行的情况下绘制侧边栏背景。我们添加到节号的右填充现在位于旋转后的元素的顶部。

缩放和倾斜

到目前为止,我们已经研究了 translate()和 rotate()转换函数。在剩下的 2D 变换中,只剩下缩放()和倾斜()。这两者都具有在单个轴上缩放和倾斜的相应功能。就像 translate 一样,我们有 scaleX()、scaleY()、skewX()和 skewY()

使用 scale()函数非常简单。scale()函数中使用无单位数字。它接受一个或两个数字。如果只使用一个元素,则该元素在 x 轴和 y 轴上的缩放比例相等。例如,两个轴的比例都为 2,意味着元素的宽度和高度都是原来的两倍。比例测量值为 1 表示元素未改变。

.doubled {
  transform: scale(2);
  /* ...is equivalent to transform: scale(2, 2);
  /* ...and also equivalent to transform: scaleX(2) scaleY(2); */
}

仅缩放一个轴意味着元素挤压(见图 10-11 )或伸展。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-11。缩放到 x 轴上小于 1 的尺寸的文本会被挤压
.squashed-text {
  transform: scaleX(0.5);
  /* ...equivalent to transform: scale(0.5, 1);
}

倾斜意味着元件的水平或垂直平行边缘相对于彼此移动一定的度数。很容易把 x 轴和 y 轴搞混——在 x 轴上歪斜意味着水平线还是水平的,而垂直线是倾斜的。关键是考虑你希望边相对于彼此移动到哪个轴。

回到搏击俱乐部的例子,我们可以使用倾斜来创建一个流行的“2.5D”效果,也许是受复古视频游戏的启发(它的花哨名称似乎是轴测投影)。

如果我们给列表项交替的背景和边框颜色以及交替的倾斜变换,我们会得到一个“手风琴”式表面的外观(见图 10-12 ):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-12。使用倾斜变换创建“2.5D”外观
/* some properties omitted for brevity */
.rules li {
  transform: skewX(15deg);
}
.rules li:nth-child(even) {
  transform: skewX(-15deg);
}

2D 矩阵变换

正如我们在本节开始时所讨论的,变换导致变换元素表面上的每个点都经过一次计算,该计算确定了它在局部坐标系中的最终位置。

当我们写 CSS 的时候,我们会用“围绕它的中心旋转这个元素,并向左上方移动它”这样的术语来思考。对于浏览器来说,我们应用的所有这些转换都被融合到一个数学结构中,称为转换矩阵。您可以使用底层的 matrix()函数,使用六个不同数值的组合来直接操作它的值。

现在,不要担心:这不是你通常会用手做的事情,因为任何超出单个缩放或平移操作的事情都需要相当的数学技能。

下面是 matrix()函数的一个应用程序,它等于将一个元素旋转 45 度,然后将其放大两倍,然后在旋转后的 x 轴上平移 100 个像素,最后在 x 轴上倾斜 10 度。乍看之下,得到的数字(为了节省空间,已经进行了一定程度的四舍五入)与单个变换的原始值几乎没有相似之处。

.box {
  width: 100px;
  height: 100px;
  transform: matrix(1.41, 1.41, -1.16, 1.66, 70.7, 70.7);
  /* equivalent to:
     transform: rotate(45deg) translate(100px, 0) scale(2) skewX(10deg); */
}

不太容易解释,是吗?

对于所有的意图和目的,转换矩阵是一个“黑盒”,需要输入各种数字来表示最终的转换,这可能是几个步骤的组合。我们可以预先计算这些值(如果我们知道数学的话),然后将它们输入 matrix()函数,但是我们无法查看 matrix()函数的值并知道其中包含了哪些单独的转换。

关键点在于,单个矩阵可以简洁地表示任意数量的变换的组合*。matrix()函数的主要用例不是为了节省空间和炫耀数学技能——但是当与 JavaScript 结合使用时,它真的会大放异彩。事实上,当您在一个元素上设置一个转换并在 JavaScript 文件中请求转换的计算样式时,您会得到一个矩阵表示。*

由于矩阵可以由脚本非常有效地操纵,然后插回到 matrix()函数中,许多基于 JavaScript 的动画库大量使用它。如果你正在手工编写 CSS,那就简单多了(可读性也更好!)坚持使用正常的转换函数。

如果你想了解更多关于如何操作 CSS 变换矩阵背后的数学知识,你可以在www . useragentman . com/blog/2011/01/07/css3-matrix-transform-for-the-the-mathematical-challenged/找到 Zoltan·霍利卢克的精彩介绍。

转换和性能

当浏览器计算 CSS 如何影响页面上的元素时,有些东西在性能方面比其他东西更昂贵。例如,如果更改文本大小,生成的行框可能会随着文本换行而变得不同,然后元素可能会变高。变高会推低页面上的其他元素,这反过来又会迫使浏览器做进一步的重新计算。

当您使用 CSS 变换时,这些计算只影响应用它的元素的坐标系,既不改变元素内部的布局,也不改变元素外部的布局。此外,这种计算几乎可以独立于页面中的所有其他事情(如运行脚本或布置其他元素)来完成,因为转换不太可能干扰这些。大多数浏览器也试图让图形处理器来处理这些事情(如果有的话),因为它是专门为这种数学而构建的。

这意味着从性能的角度来看,转换是非常好的。任何时候,当你想创造一个可以通过变换复制的效果时,它很有可能会有更好的表现。快速连续地进行多次变换会增加收益,例如在制作元素动画或变换元素时。

一些最终转换“抓住了”

转换具有很好的性能,并且非常容易使用。也就是说,使用转换有一些意想不到的副作用:

  • 有些浏览器会为转换后的元素切换抗锯齿方法。这意味着当动态应用转换时,像文本呈现这样的事情看起来会突然不同。为了解决这个问题,您可以尝试应用一个仅使用初始值的转换,在页面加载时将元素保留在原位。这样,渲染甚至会在应用最终变换之前切换。

  • 应用于元素的任何变换都会创建新的堆叠上下文。这意味着在组合 z-index 和 transform 时需要小心,因为转换后的元素会创建自己的堆栈——即使在转换后的元素内将子元素的 z-index 设置得很高,它们也不会位于元素外的元素之上。

  • 变换后的元素还为固定定位建立了新的包含块。如果已转换的元素中包含 position: fixed 元素,它会将已转换的元素视为其视口。

过渡

转换是从一种状态到另一种状态的自动动画,例如当按钮从正常状态变为按下状态时。通常情况下,这种变化会立即发生,或者至少以浏览器最快的速度发生。当你点击一个按钮时,浏览器会计算出页面的新外观,并在几毫秒内绘制出来。当你应用一个过渡时,你告诉浏览器这个变化需要多长时间,浏览器会计算在这段时间内屏幕应该是什么样子。

过渡会自动在两个方向上运行,因此一旦状态反转(就像释放按钮时),动画就会反向运行。

让我们从表单章节(第九章)中的按钮来说明这一点,并为它创建一个平滑的按下状态动画。我们的目标是通过将按钮在页面上向下移动几个像素来创建按钮被按下的外观,并减少阴影的偏移来进一步增强按钮消失在页面后面的错觉(见图 10-13 ):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-13。正常和:按钮的活动状态
<button>Press me!</button>

以下是第九章中按钮代码的基础(为简洁起见,省略了一些属性)。这一次,我们向规则添加了转换属性。

button {
  border: 0;
  padding: .5em 1em;
  color: #fff;
  border-radius: .25em;
  background-color: #173b6d;
  box-shadow: 0 .25em 0 rgba(23, 59, 109, 0.3), inset 0 1px 0 rgba(0, 0, 0, 0.3);
**transition: all 150ms;** 
}
button:active {
**box-shadow: 0 0 0 rgba(23, 59, 109, 0.3), inset 0 1px 0 rgba(0, 0, 0, 0.3);** 
t**ransform: translateY(.25em);** 
}

当按钮被激活时,我们将其向下平移与 y 轴阴影偏移相同的距离。同时,我们减少了阴影偏移。通过使用 transform 属性来移动按钮,我们避免了强制页面回流。

前面的代码还告诉按钮使用一个转换来更改所有受影响的属性,并且更改应该在 150 毫秒或 0.15 秒内发生。使用动画向我们介绍了新的与时间相关的单位:ms 代表毫秒,s 代表秒。用户界面组件中的大多数过渡应该低于 0.3 秒,否则会感觉迟钝。其他视觉效果可能需要更长时间。

transition 属性是一种允许我们一次设置几个属性的简写方式。设置转换的持续时间并告诉浏览器转换所有在状态之间改变的属性也可以通过以下方式完成:

button {
  transition-property: all;
  transition-duration: .15s;
}

如果我们只想专门转换 transformand 和 box-shadow 属性,而其他更改(例如,不同的背景颜色)应该立即发生,我们将不得不指定单个属性,而不是所有属性。

我们不能为单个转换指定一个以上的属性名,但是我们可以指定几个转换,用逗号分隔。这意味着我们可以重复相同的值,但是对于不同的属性关键字:

button {
  transition: box-shadow .15s, transform .15s;
}

请注意,我们现在必须在两个转场中重复持续时间。这种重复是以后事情不同步的原因。不要重复自己(简称 DRY)是写好代码的一个基本准则。当过渡更复杂时,最好单独设置 transition-property 以避免重复:

button {
  /* First, specify a list of properties using transition-property */
  transition-property: transform, box-shadow;
  /* Then, set values that go for those properties. */
  transition-duration: .15s;
}

当您在转换声明中使用多个逗号分隔的值时,它们的工作方式类似于多个背景属性。transition-property 中的列表决定了要应用的过渡数量,如果其他列表较短,它们会重复。

在前面的示例中,过渡持续时间只有一个值,但定义了两个过渡属性,因此持续时间值适用于这两个属性。

注意

当转换带前缀的属性时,必须将带前缀的属性名作为转换属性。例如,transition: transform .25s 通常需要用-webkit-transition:-WebKit-transform. 25s 来补充,在基于 WebKit 的旧浏览器中,transition 和 transform 都有前缀。

过渡时序功能

默认情况下,转场的变化速率在帧与帧之间并不完全相同,它开始时稍慢,然后快速加速,最后逐渐减速,直到达到最终值。

这种移动速度在动画术语中被称为放松,它通常会让变化看起来更自然、更平稳。有一些数学函数负责创建这种变量变化,并使用转换时间函数属性来控制它们。

有一些关键字代表不同样式的计时功能。前面描述的缺省值称为 ease。其他的是线性、渐进、渐出和渐出。

“放松”意味着开始时缓慢,然后加速。“放松”的意思正好相反:开始时快速,然后在结束时慢下来。最后,双管齐下会让我们在开始和结束时缓慢变化,但在中间会加速变化。

将结果可视化在书页上很难,但是图 10-14 中的插图应该会给你一个想法。它代表一个矩形,我们在一秒钟内将背景颜色从黑色变为白色。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-14。在 1 秒钟的动画中,采样停止点之间相隔 100 毫秒

如果我们想改变按钮动画来使用渐强计时功能,我们可以这样做:

button {
  transition: all .25 ease-in;
  /* ...or we could set transition-timing-function: ease-in; */
}
三次贝塞尔函数和“弹性”过渡

在幕后,处理变化率的数学函数建立在一种叫做三次贝塞尔函数的东西上。每个关键字都是使用这些带有特定参数的函数的快捷方式。通常情况下,使用这些函数的时间变化被视为一条曲线,从初始时间和初始值(左下角)到持续时间结束时的最终值(右上角),如图 10-15 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-15。显示渐出转场定时功能的曲线

一个三次贝塞尔函数需要四个参数来计算随时间的变化,我们可以使用三次贝塞尔()函数作为 CSS 转换中的缓动值。这意味着您可以通过计算和填充这四个值来创建自己的计时函数。这四个值代表形成该曲线的两个控制点的两对 x 和 y 坐标。

就像矩阵变换一样,这不是你通常手工做的事情,因为它需要高级数学技能。幸运的是,还有其他人使用这些技能为我们其他人创造工具!Lea Verou 专门为 CSS 编写了一个这样的工具,可在cubic-bezier.com获得(见图 10-16 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-16。在cubic-bezier.com,你可以玩不同赛季的预设值,并创建自己的预设值

使用自定义计时功能的一个更有趣的结果是,您可以在转换时将值更改到起始值和结束值之外(如图 10-16 所示)。例如,在实践中,这意味着当你移动某物时,在最终停止之前超过你的目标。这使您有可能创建有弹性的过渡,其中元素看起来有弹性或咬合到位。试着在 http://cubic-bezier.com 的上玩这个例子看看效果!

阶跃函数

除了使用预设关键字和立方贝塞尔()函数指定缓动之外,您还可以创建逐步发生的过渡。这对于创建定格动画非常有用。假设有一个元素,它的背景图像由七个不同的图像组成,都在同一个文件中。定位图像,以便仅显示其中一个(参见图 10-17 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-17。使用背景位置的七帧定格动画

当我们将鼠标悬停在该元素上时,我们希望通过移动 background-position 属性来动画显示背景图像。如果我们使用线性或缓和的过渡来做这件事,背景图像将会滑过,破坏这种错觉。相反,我们需要它以六个不连续的步骤进行过渡:

.hello-box {
  width: 200px;
  height: 200px;
  transition: background-position 1s **steps(6, start)**;
  background: url(steps-animation.png) no-repeat 0 -1200px;
}
.hello-box:hover {
  background-position: 0 0;
}

transition-timing-function 现在设置为 steps(6,start),意思是“将转换持续时间分成六个步骤,并在每个新步骤开始时更改属性。”总而言之,我们得到七个不同的帧,包括起始状态。

默认情况下,steps(6)会在每一步的 end 处更改属性,但是如果您将 start 或 end 作为第二个参数传入,您可以明确地说明这一点。因为我们想在用户悬停在元素上时直接看到变化,所以我们选择在每一步开始时开始转换。

现在,转换的 steps()函数有一个问题。当您在过渡完成之前反转状态时(例如,通过快速移开鼠标指针),过渡将向后播放。这是意料之中的,但出乎意料的是,反向转换仍然有六个步骤。这些步骤现在不再映射到仔细考虑过的背景位置,使我们的动画看起来很糟糕。

可悲的是,这在当前版本的规范中是未定义的行为,所有浏览器似乎都以这种可以说是糟糕的方式对待 step 函数。为了应对这种糟糕的体验,我们可以使用一些有用的过渡技巧,这将在下一节中介绍。

正向和反向的不同转换

有时,我们希望某些东西在一个方向上快速过渡,而在另一个方向上缓慢过渡,反之亦然。在前面的步进示例中,当悬停状态在转换完成之前中止时,我们不能优雅地后退。我们可以通过立即恢复过渡来解决这个问题。

为此,我们需要定义不同的转换属性集:一个用于未悬停状态,一个用于悬停状态。诀窍是把正确的放在正确的地方。

我们给初始转换一个 0 的持续时间,然后设置“真正的”转换在元素悬停时发生。现在悬停状态触发动画,当悬停被取消时,图像会迅速恢复。

.hello {
  transition: background-position **0s** steps(6);
}
.hello:hover {
  transition-duration: **.6s**;
}

“粘性”过渡

转换的另一个技巧是让转换完全不反转,这与我们之前的例子相反。为了进行“粘性”转换,我们可以使用一个长得离谱的转换持续时间。从技术上来说,当我们取消悬停时,它仍然会向后运行,但非常非常慢——你必须让浏览器标签打开多年才能看到任何变化!

.hello {
  transition: background-position **9999999999s** steps(6);
}
.hello:hover {
  transition-duration: **0.6s**;
}

延迟转换

通常,状态一改变,元素就开始转换——例如,当一个类名被 JavaScript 改变或者一个按钮被按下时。我们可以选择使用 transition-delay 属性来延迟这个转换。例如,我们可能只想在用户将指针悬停在停止动画上超过一秒钟时运行停止动画。

就值的顺序而言,转换的简写相当宽松,但是延迟必须是第二次出现的时间值,第一次出现的时间值将被解释为持续时间。

.hello {
  transition: background-position 0s **1s** steps(6);
  /* equivalent to adding transition-delay: 1s; */
}

你也可以使用负延迟。遗憾的是,这并不能实现时间旅行,但它可以让你从一开始就直接跳到过渡的中途。如果在 10 秒的过渡上设置过渡延迟:-5s,则当触发过渡时,它将立即跳到中间标记。

你能做什么,不能做什么

到目前为止,我们已经转换了变换、框阴影值和背景位置。但是并不是每个 CSS 属性都可以被动画化,也不是每个值都可以。大多数情况下,使用长度或颜色的就可以了:边框、宽度、高度、背景颜色、字体大小等等。关键是你能否计算出它们之间的一个值。您可以在 100px200px 之间以及红色蓝色之间找到中间值(因为颜色也是幕后的数值),但不能在例如显示属性的 block 和 none 之间找到中间值。这条规则也有一些例外。

插值

尽管没有明确的中间值,但有些属性是可以动画化的。例如,当使用 z-index 时,值不能是 1.5,但 1 或 999 就可以了。对于许多属性,比如只接受整数值的 z-index 或 column-count,浏览器会将它们插值成整数,有点像前面的 steps()函数。

一些可以插值的值有点令人惊讶。例如,您可以转换 visibility 属性的值,但是一旦转换通过两个结束状态之间的中点,浏览器就会将该值“捕捉”到两个结束状态中的任一个。

设计师奥利·斯图德霍尔姆有一个很方便的属性列表,既有 CSS 规范中的属性,也有 SVG 中的属性,可以通过 CSS 制作动画:oli.jp/2010/css-animatable-properties/

过渡到内容高度

转换的最后一个缺陷是,可以转换的属性,比如高度,只能在数值之间转换。这意味着其他关键字,如 auto,不能表示为要转换到的状态之一。

一种常见的模式是拥有一个折叠的元素,当用户与它交互时,您可以将它转换为完整的高度,就像手风琴组件一样。浏览器不知道如何在像 0 这样的长度和关键字 auto 之间转换,甚至不知道如何在像 max-content 这样的固有测量关键字之间转换。

在图 10-18 中,我们有一个餐馆菜单组件,最初显示了前三个菜单选项。当我们切换列表的其余部分时,它应该向下滑动并淡入。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-18。扩展菜单列表组件

在这种情况下,我们知道列表的大致高度,因为它总共有 10 个条目——它仍然会有一些变化,因为可能会有带换行符的长名称。我们现在可以用最大高度来代替它。使用这种技术,我们从最初设置的尺寸到一定比元素扩展高度高的长度。在这种情况下,我们决定将其余的“顶部菜单选项”限制为另外七个项目。

组件的标记基于两个有序列表,其中第二个列表从数字 4 开始:

<div class="expando">
  <h2 class="expando-title">Top menu choices</h2>
  <ol>
    <li>Capricciosa</li>
    <li>Margherita</li>
    <li>Vesuvio</li>
  </ol>
  <ol class="expando-list" start="4" aria-label="Top menu choices, continued.">
    <li>Calzone</li>
    <!-- …and so on… -->
    <li>Fungi</li>
  </ol>
</div>

该标记在第二个列表中包含 aria-label 属性,以便让屏幕阅读器的用户清楚这两个列表的用途。

为了切换状态,我们使用一小段 JavaScript 来设置场景。在运行的示例中,您可以找到这个脚本,它为我们创建了一个按钮,将它附加到标题,并在单击按钮时切换容器元素上 is-expanded 的类名。

它还向 html 元素添加了一个类名 js。然后,我们可以将我们的样式基于这些类名的存在,因此如果 JavaScript 不运行,用户将从一开始就看到完整的扩展列表。

.js .expando-list {
  overflow: hidden;
  transition: all .25s ease-in-out;
**max-height: 0;** 
**opacity: 0;** 
}
.js .is-expanded .expando-list {
**max-height: 24em;** 
**opacity: 1;** 
}

扩展的 max-height 被设置为一个比实际列表的预期最大高度大很多的值。这是为了有一个安全余量:例如,如果在小屏幕上的菜单项中有几个意外的换行符,我们不希望列表被最大高度截断。

小缺点是 max-height 转换仍然会运行,就好像元素正好是 24 ems 高一样,这使得缓和点和停止点超出了列表的整个高度。如果您使用该示例,这在塌陷动画中作为一个小延迟是最明显的。在一个更健壮的例子中,脚本最初可以过渡到一个非常高的 max-height,然后在过渡之后测量元素*,以基于内容动态更新 max-height。*

CSS 关键帧动画

CSS 转场是隐式动画。我们为浏览器提供了两种不同的状态,当一个元素从一种状态转换到另一种状态时,包含在转换中的任何属性都将被激活。有时,我们需要做的不仅仅是在两种状态之间动画化,或者明确地动画化某些可能一开始就不存在的属性。

CSS 动画规范允许我们使用关键帧的概念来定义这些种类的动画。此外,它们允许我们控制动画运行的其他几个方面。

让生活的幻觉栩栩如生

使用动画的好处之一是通过展示而不是讲述来传达信息。我们可以用它来引导注意力(就像移动的箭头告诉你“看这里!这很重要!”),解释刚刚发生的事情(例如,当使用淡入动画来显示添加了列表项时),或者只是为了使我们的网页看起来更生动一些,以建立情感联系。

华特·迪士尼工作室教授通过动画表达性格和个性的 12 条原则。这些后来被收集在一本名为《生活的幻觉》的书中。动画师文森佐·洛迪贾尼用一个小立方体作为主角,创作了一部动画短片来说明这些原则(【https://vimeo.com/93206523】)。去看看吧!

受此启发,我们将创建一个动画方形标志,展示一些关键帧动画可以做什么。徽标的静态渲染由单词“Boxmodel”旁边的一个正方形组成(见图 10-19 ),这将是我们虚构的公司名称。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-19。静态徽标

标记非常简单:一个 heading 元素和一些额外的 span 元素来包装单词,两个嵌套的 span 元素来表示小方块。使用额外的空元素来表示并不理想,但是为了达到我们想要的效果,这是必要的,原因将变得很清楚。

<h1 class="logo">
  <!-- This is the box we are animating -->
  <span class="box-outer"><span class="box-inner"></span></span>
  <span class="logo-box">Box</span><span class="logo-model">model</span>
</h1>

对于基本的样式,我们给页面一个背景颜色,给 logo 一些字体属性,并设置正方形的尺寸和颜色。我们通过将表示正方形的两个 span 元素的显示模式设置为 inline-block 来准备动画,因为不可能转换内联文本元素。

body {
  background-color: #663399;
  margin: 2em;
}
.logo {
  color: #fff;
  font-family: Helvetica Neue, Arial, sans-serif;
  font-size: 2em;
  margin: 1em 0;
}
.box-outer {
  display: inline-block;
}
.box-inner {
  display: inline-block;
  width: .74em;
  height: .74em;
  background-color: #fff;
}
创建动画关键帧块

接下来,我们需要创建实际的动画。我们想模仿“生命的幻觉”电影的开场序列,小方块挣扎着滚过屏幕。

CSS 动画在语法和结构上有点奇怪。使用@keyframes 规则定义和命名动画序列,然后使用 animation-*属性将该序列连接到 CSS 中的一个或多个规则集。

下面是第一个关键帧块的外观:

@keyframes roll {
  from {
    transform: translateX(-100%);
    animation-timing-function: ease-in-out;
  }
  20% {
    transform: translateX(-100%) skewX(15deg);
  }
  28% {
    transform: translateX(-100%) skewX(0deg);
    animation-timing-function: ease-out;
  }
  45% {
    transform: translateX(-100%) skewX(-5deg) rotate(20deg) scaleY(1.1);
    animation-timing-function: ease-in-out;
  }
  50% {
    transform: translateX(-100%) rotate(45deg) scaleY(1.1);
    animation-timing-function: ease-in;
  }
  60% {
    transform: translateX(-100%) rotate(90deg);
  }
  65% {
    transform: translateX(-100%) rotate(90deg) skewY(10deg);
  }
  70% {
    transform: translateX(-100%) rotate(90deg) skewY(0deg);
  }
  to {
    transform: translateX(-100%) rotate(90deg);
  }
}

它确实很长,但是有很多重复的地方。首先,我们将关键帧序列命名为 roll——这可以是任何有效的标识符,只要它不与 CSS 中任何预定义的名称冲突。我们还没有决定这个动画需要多长时间,所以使用关键帧选择器选择块内的时间点,以百分比的形式写在时间轴上。

我们还可以使用特殊的关键字 from 和 to,它们分别是 0%和 100%的别名。如果缺少 from(或 0%)或 to(或 100%),它们将根据元素现有属性的初始状态自动构建。您可以拥有的关键帧选择器的数量从一个到您需要的任何数量,由您决定。

第一个关键帧(0%)设置动画计时功能属性。它的工作原理与过渡类似:使用预置关键字进行缓动,或使用三次贝塞尔()函数。在关键帧选择器中设置计时功能可以控制这个关键帧和下一个关键帧之间的过渡时间。

我们还使用 translateX(-100%)将正方形的起始位置移动到其自身左侧的 100%。

接下来,我们设置应用各种变换的整个范围的关键帧,以及单独的定时功能。图 10-20 显示了元素在每个关键帧中的样子。请注意,有些关键帧是相同的,例如在结尾:这是为了控制动画的速度。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-20。我们动画的各种关键帧

该元素将首先倾斜一点,好像是为了聚集动量,然后旋转和拉伸,几乎在 45 度角处停止,最终完成 90 度旋转,并在旋转轴线上倾斜一点,进行弹性停止。那是我们的第一部动画。

将关键帧块连接到元素

现在我们已经定义了一个动画关键帧序列,我们需要将它连接到徽标中的方块。正如转场属性一样,也有控制持续时间、延迟和计时功能的动画属性,但还有一些附加控件:

.box-inner {
  animation-name: roll;
  animation-duration: 1.5s;
  animation-delay: 1s;
  animation-iteration-count: 3;
  animation-timing-function: linear;
  transform-origin: bottom right;
}

我们将 animation-name 应用于该元素,以使用滚动动画。使用动画持续时间,我们设置每次迭代的时间。animation-delay 属性告诉浏览器在运行动画之前等待 1 秒钟。我们希望盒子在停止前翻转三次,所以我们将动画-迭代-计数设置为 3。

我们可以在关键帧选择器和动画元素上设置动画计时功能。这里,定时功能在整个序列中设置为线性,但我们已经看到如何在单个关键帧之间覆盖它。

注意

您可以使用与过渡相同的逗号分隔语法将多个动画应用到同一元素。如果两个动画试图同时制作同一属性的动画,则最后声明的动画获胜。

最后,我们将 transform-origin 属性设置为 bottom right,因为我们希望正方形以其右下角为轴心。

使用速记动画属性,我们可以将前面的所有细节浓缩成一行,就像过渡一样:

.box-inner {
**animation: roll 1.5s 1s 3 linear;** 
  transform-origin: bottom right;
}

但是我们还没完。到目前为止,我们有一个广场,重复滚动动画到位。我们需要它从视窗外移动到最终目的地。这在使用单个动画时是可能的,但是使用大量的关键帧。相反,我们可以在外部跨度元素上应用另一个动画和另一组变换。这一次,动画简单多了。我们希望它从左边移动,移动的距离是盒子宽度的三倍:

@keyframes shift {
  from {
    transform: translateX(-300%);
  }
}

因为我们想要从某个东西到初始状态的动画,我们可以省略关键帧,只留下从状态。

关于关键帧块和前缀的注释

我们在这一章中保持了各种属性的标准化的、无前缀的版本。代码示例具有完整的带前缀的代码。

在带有动画属性前缀的浏览器中,关键帧规则也带有前缀。这意味着您必须为每个前缀编写一套关键帧规则!幸运的是,现在大多数浏览器都接受无前缀的版本,所以通常只需要添加-WebKit-前缀。

现在,我们可以使用步进时序函数将移位序列应用于外部跨度。共有三个步骤,因此每次滚动动画结束并将正方形恢复到初始位置时,步进函数都会将其向前移动相同的量。这就是产生正方形在屏幕上滚动的错觉的原因;这很难说明,但是可以尝试一下代码示例,看看它是如何工作的。

.box-outer {
  display: inline-block;
**animation: shift 4.5s 1s steps(3, start) backwards;** 
}

最后一个关键字 backwards 设置动画序列的动画填充模式属性。填充模式告诉浏览器如何在动画运行之前或之后处理动画。默认情况下,在动画运行之前,不会设置第一个关键帧中的属性。如果我们提供 backward 关键字,这些值会在时间上向后填充,因此即使动画最初被延迟或暂停,第一个关键帧属性也会立即设置。向前填充使序列中的最后一个值在时间上保持向前,并且向前和向后填充。

在这种情况下,我们希望动画立即离开屏幕,但保留最终值(因为它与盒子的初始位置相同),所以我们向后填充。

至此,我们的第一个关键帧动画完成了。当你加载这个例子时,这个小方块高兴地在屏幕上挣扎。

沿着曲线制作动画

根据定义,对两点之间的元素位置设置动画会使其沿直线移动。您可以通过创建大量关键帧来创建曲线运动的外观,每次都略微改变方向。更好的方法是通过以特定顺序组合旋转和平移来移动对象,就像 Lea Verou 的这个示例:Lea . Verou . me/2012/02/moving-an-element-along-a-circle/

在示例文件中,我们包含了一个加载动画的示例,它使用这种技术来演示文件上传到服务器的过程。这些文件沿着一个半圆形的路径从计算机“跳到”服务器图标上,同时缩小一点以适应服务器图标的后面(见图 10-21 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-21。文件图标沿着弯曲的路径移动到服务器图标

这是该动画的关键帧块:

@keyframes jump {
  from {
    transform: rotate(0) translateX(-170px) rotate(0) scale(1);
  }
  70%, 100% {
    transform: rotate(175deg) translateX(-170px) rotate(-175deg) scale(.5);
  }
}

初始关键帧将文件元素向左平移 170 个像素(以便在计算机图标上重新开始)。第二个关键帧选择器将元素旋转 175 度,仍然平移相同的量,然后以相反的方向将其旋转 175 度。由于这是在平移位置完成的,它用于保持元素直立,因此在旋转时不会上下颠倒。最后,我们将元素缩小到一半大小。

图 10-22 展示了这种特殊的变换组合是如何沿着弧线运动的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-22。由于旋转是在平移之前应用的,因此图标沿弧线移动。这是它在动画中大约四分之一处旋转 45 度后的样子。

然后,我们将这个动画连接到 file-icon 元素,并设置持续时间和放松功能。由于是加载动画,所以我们设置为无限重复(我们都经历过!),通过添加关键字 infinite 作为 animation-iteration-count 值。

.file-icon {
**animation: jump 2s ease-in-out infinite;** 
}

您可能已经注意到,最终的关键帧选择器同时针对动画的 70% 标记和 100% 标记。这是因为我们希望动画在重新开始之前暂停一会儿。

没有特定的属性来控制这种延迟,所以我们希望 70%和 100%的状态保持不变,我们可以以这种方式组合具有完全相同属性的关键帧,就像我们组合普通的逗号分隔选择器一样。

动画事件、播放状态和方向

在某个时候,文件传输应该完成了,希望如此。在完整的代码示例中,我们添加了按钮来模拟动画的结束、重新开始和暂停。它们唯一的功能是给文件图标添加两个类名中的一个。这些类将属性 animation-play-state 添加到文件图标,并将其设置为 paused。这个属性有两个值:默认情况下,它被设置为 running。

停止动作不同于暂停,因为它与动画开始、停止或启动新的迭代时触发的 JavaScript 事件挂钩。动画完成当前迭代后,文件图标会消失,服务器图标旁边会出现一个复选标记。您可以研究这个示例的源代码,看看它是如何工作的,或者在 MDN 上阅读更多关于 JavaScript 动画事件的内容(developer . Mozilla . org/en-US/docs/Web/API/animation event)。

最后,您还可以使用 animation-direction 属性来控制动画的方向。默认情况下,它被设置为正常,但是您可以使用 reverse 关键字反向运行动画,并且您将有一个免费的“下载”动画!

还有 alternate 和 alternate-reverse 关键字,它们在动画迭代之间改变方向。它们之间的区别是交替开始于正常方向,而交替-反向开始于反向。

一些动画“Gotchas”

当使用 CSS 关键帧动画时,有相当多的陷阱和不一致。以下是一些值得了解的信息:

  • 一些动画在页面加载时就开始运行,尽管会有一点延迟。这可能很棘手,因为一些浏览器在一开始就运行流畅时会出现错误行为;如果您在一些不同的浏览器中查看滚动正方形示例,您有时会注意到这一点。当一切就绪时,使用 JavaScript 触发动画通常更好。

  • 关键帧中的属性没有任何特异性。它们只是改变它们所应用到的元素的属性。尽管如此,一些浏览器(但不是全部)允许您用!正常规则中的重要标志,来自动画内部,这可能会令人困惑。

  • 相反,在关键帧块中设置的属性不允许用!重要的旗帜。关键帧块中设置了该标志的任何声明都将被忽略。

  • Android 操作系统的版本 2 和 3 支持 CSS 动画,但是一次只能支持一个属性!如果您尝试制作两个或更多属性的动画,该元素将完全消失。为了解决这个问题,您可以将动画分割成单独的关键帧块。

3D 转换

现在我们已经使用了常规的 2D 变换、过渡和动画,是时候看看 CSS 工具包中可能最令人印象深刻的工具:3D 变换了。

我们已经学习了 2D 空间中变换和坐标系的基础知识。当我们转向 3D 时,我们正在处理完全相同的概念,但这一次,我们还必须考虑 z 维度。3D 变换允许我们将坐标系旋转、倾斜、缩放或移动远离我们。为了达到这种效果,有必要引入透视的概念。

获得一些观点

当处理 3D 时,我们需要在三个轴上表示变换。x 和 y 轴仍然代表同样的东西,但是 z 代表一条穿过屏幕并朝向我们观众的线,可以这么说(见图 10-23 )。屏幕本身的表面通常被称为 z 平面,这是 z 轴上的默认位置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-23。3D 坐标系中的 z 轴

这意味着,当我们远离它们(z 轴的负方向)时,它们需要显得更小,当我们靠近它们时,它们需要显得更大。在 x 或 y 轴上旋转某物会使它的一部分变大,另一部分变小,等等。

让我们开始尝试一个例子。我们将使用来自 2D 部分的可靠的 100×100 像素的盒子,并围绕 y 轴旋转它:

.box {
  margin: auto;
  border: 2px solid;
  width: 100px;
  height: 100px;
  transform: **rotateY(60deg)**;
}

仅仅这样并不能让你走得更远:盒子会看起来更窄(这是在 y 轴上旋转时所期望的),但会完全缺乏任何 3D 感(参见图 10-24 的最左边部分)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-24。我们旋转的 100×100 像素的盒子,没有透视(左),透视:140 像素(中),透视:800 像素(右)

原因是我们还没有定义一个视角:我们必须选择我们应该出现在离盒子多远的地方。你离一个物体越近,变化就越明显,离得越远,变化就越小。默认的视角基本上是无限远,所以我们没有得到非常明显的效果。

我们通过在要转换的元素的父元素上设置 perspective 属性来解决这个问题:

body {
  perspective: 800px;
}

这个测量值表示视点应该位于离屏幕多远的地方。您必须根据具体情况进行实验以找到正确的值,但是大约 600 到 1000 像素是一个很好的起点。

透视原点

默认情况下,假定查看者的视角以应用了该视角的元素为中心。从技术上讲,这意味着消失点在中心。您可以使用透视原点属性对此进行控制。它的工作方式类似于 transform-origin 属性:您为它提供一对带有关键字(上、右、下、左)、百分比或长度的 x 和 y 坐标值。

图 10-25 展示了三维物体在物体元素上的透视。所有的盒子都在 x 轴上旋转了 90 度(所以它们都是朝上的),但是左边和右边的图像有不同的透视原点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-25。左侧的浏览器窗口有一个默认的透视原点(50% 50%),右侧的透视原点设置为左上角
Perspective()转换函数

在父元素上设置 perspective 属性会使其内部元素上的所有 3D 转换共享同一个透视图。这通常是你想要的,因为它能产生更真实的效果。

perspective()函数允许您在每个变换的元素上设置单独的透视图。通过下面的方法可以得到与前面的例子相似的结果,但是透视图不能在元素之间共享:

.box {
  transform: perspective(800px) rotateY(60deg);
}

创建 3D 微件

现在我们有了移动东西并以 3D 视角显示它们的方法,我们可以创建一些更有用的东西。除了使用动作来增加一点趣味或解释正在发生的事情,我们还可以结合动作和 3D 来节省空间,同时整理设计。

我们的目标是使用 CSS 和 JavaScript 构建一个 3D 小部件,其中用户界面的一些部分隐藏在元素的背面。我们将重用前面的菜单组件,并添加过滤选项,而不是展开所有项目。通过点击“显示过滤器”按钮,元件将翻转 180 度并显示背板(参见图 10-26 )。点击“给我看看披萨!”再次翻转它,在现实世界的例子中,比萨饼列表现在将根据复选框的选择进行过滤。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-26。我们的“可翻转”部件

首先,对于不支持 3D 转换的浏览器,或者当 JavaScript 不能正常运行时,我们需要一些可靠的标记和默认情况。当浏览器不支持 3D 变换时,我们可以在页面上一个接一个地显示正面和背面,如图 10-27 所示。理论上,点击“给我看看披萨!”按钮将简单地重新加载应用了新过滤器的页面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-27。基本的“2D”版本在页面中一个接一个地显示了小部件的两面

标记类似于本章前面的菜单,但是我们添加了一些新的类名和一个包装器来保存整个结构。

<div class="flip-wrapper menu-wrapper">
  <div class="flip-a menu">
    <h1 class="menu-heading">Top menu choices</h1>
    <ol class="menu-list">
      <li>Capricciosa</li>
      <!-- ...and so on, all 10 choices -->
    </ol>
  </div>
  <div class="flip-b menu-settings">
    <!-- the form on the back of the widget goes here. -->
  </div>
</div>

我们将使用 Modernizr 来检测对 3D 转换的支持,因此当支持 CSS 3D 转换时,增强的小部件的规则将以添加到 html 元素中的类名作为“前缀”。

首先,我们将在 body 元素上设置透视图,并使 wrapper 元素成为其后代的定位上下文。然后我们将添加转换,目标是包装器的 transform 属性。

.csstransforms3d body {
  perspective: 1000px;
}
.csstransforms3d .flip-wrapper {
  position: relative;
  transition: transform .25s ease-in-out;
}

现在,我们将使小部件背面的内容绝对定位,使其覆盖与正面相同的空间,并在 y 轴上将它翻转 180 度。我们也希望双方都是不可见的,当他们以错误的方式翻转时,这样一方就不会遮住另一方。我们将使用背面可见性属性来控制这一点;它默认为可见,但将其设置为隐藏会使元素在从后面看时不可见。

.csstransforms3d .flip-b {
  position: absolute;
  top: 0;  left: 0;  right: 0;  bottom: 0;
  margin: 0;
  transform: rotateY(-180deg);
}
.csstransforms3d .flip-b,
.csstransforms3d .flip-a {
  backface-visibility: hidden;
}

当我们旋转小部件时,我们希望整个东西都被旋转,包括已经翻转的背面。默认情况下,应用于父元素的任何 3D 变换都会使子元素上的 3D 变换无效,从而使它们变平。我们需要创建一个 3D 上下文,其中子对象的变换发生在与父对象相同的 3D 空间中。我们通过将包装元素上的 transform-style 属性设置为 preserve-3d 值来实现这一点:

.csstransforms3d .flip-wrapper {
  position: relative;
  transition: all .25s ease-in-out;
  **transform-style: preserve-3d;** /* default is flat */
}

现在拼图的最后一块是让 JavaScript 在点击前后按钮时切换包装元素上的类名。添加的 is-flipped 类名会触发整个小部件在 y 轴上旋转 180 度:

.csstransforms3d .flip-wrapper.is-flipped {
  transform: rotateY(180deg);
}

就这样,造型到位了。但是,可悲的是,现实世界中有一些约束,迫使我们重新审视这个小部件,使其能够跨浏览器兼容和访问。

IE 和 preserve-3d 的缺失

Internet Explorer 10 和 11 不支持 preserve-3d 关键字。这意味着任何元素都不能共享父元素的 3D 空间,这也意味着我们不能翻转整个小部件并让侧面跟随。我们必须单独转换每一面以使 IE 工作。

此外,IE 在父元素与多个转换元素的组合上有一些严重的错误,这意味着我们必须求助于转换列表中的 perspective()函数。

更新后的代码在小部件的前面设置一个 0 度的初始变换,在后面设置一个-180 度的变换,然后在切换包装器元素上的类名时翻转这两个变换。此外,perspective()函数需要在每一个转换链中首先引入。

.csstransforms3d .flip-b,
.csstransforms3d .flip-a {
  transition: transform .25s ease-in-out;
}
.csstransforms3d .flip-a {
  transform: perspective(1000px) rotateY(0);
}
.csstransforms3d .flip-b {
  transform: perspective(1000px) rotateY(-180deg);
}
.csstransforms3d .flip-wrapper.is-flipped .flip-a {
  transform: perspective(1000px) rotateY(180deg);
}
.csstransforms3d .flip-wrapper.is-flipped .flip-b {
  transform: perspective(1000px) rotateY(0deg);
}

iOS 8 上的 Safari 有一个相反的错误,应用了 perspective()变换的元素有时会在开始过渡时消失。一种解决方法是将多余的透视属性重新应用到 body 元素上:

.csstransforms3d .flip-wrapper {
  perspective: 1000px;
}
负责任的代码:处理键盘控制和可访问性

当开发隐藏东西的组件时,我们在前面的章节中已经看到如何隐藏它们是很重要的。简单地将某些内容旋转出视图并不会将其从例如文档的 tab 键顺序中移除。在 3D 小部件的最终代码(以及随之而来的 JavaScript 代码)中,我们加入了几个其他的修正以使小部件更加健壮:

  • 除了使用 Modernizr 来检测对 3D 转换的支持,我们还检测对 classList JavaScript API 的支持。这用于在小部件状态改变时有效地切换类名。这意味着最终代码中的所有 CSS 规则都以. csstransforms3d.classlist 为前缀。

    对 3D 转换的支持和对 classList API 的支持几乎重叠,但是我们不希望有任何边缘情况留下坏的小部件。如果浏览器不支持这两个特性,小部件将不会运行,并且“2D”样式也不会改变。

  • 当小部件的一侧被隐藏时,它会自动添加类名 is-disabled,并将 aria-hidden 属性设置为 true。is-disabled 类将 CSS 中的 visibility 属性设置为 hidden。

    这可以防止键盘用户意外跳转到他们看不到的表单控件,并防止屏幕阅读器读取内容。(aria-hidden 属性仅用于屏幕阅读器,因此不依赖于 CSS 隐藏技术。)隐藏首先发生在翻转完成之后,因此它取决于 transitionend 事件。

  • 相反,在使用类名 is-enabled 显示之前,另一侧是显式可访问的。

  • 当向后翻转小部件时,键盘焦点移回到“显示过滤器”按钮。

3D 转换的高级功能

本节介绍了 3D transforms 规范中在日常编码中可能较少使用的部分,但是提供了一些额外的功能。

Rotate3d()函数

除了单独的旋转函数——rotateX()、rotateY()和 rotateZ()(及其 2D 等价的 rotate())—还有一个名为 rotate3d()的函数。此函数允许您围绕穿过 3D 空间的任意线旋转元素,而不是在每个轴上旋转指定的量。下面是使用该函数的情况:

.box {
  transform: rotate3d(1, 1, 1, 45deg);
}

rotate3d()函数有四个参数:代表 x、y 和 z 的三个数字向量坐标和一个角度。坐标定义了空间中的一条线,旋转围绕该线发生。例如,如果向量坐标是 1,1,1,旋转将围绕一条假想的线,该线从变换原点出发,经过位于相对于原点的 x 轴(右)1 个单位、y 轴(下)1 个单位和 z 轴(朝向观察者)1 个单位处的点。

我们不需要在这里指定是哪个单位,因为这些点都是相对于彼此的——如果我们使用 100,100,100,我们会得到相同的结果,因为穿过元素的线是相同的。

实际上,3D 旋转相当于在每个轴上的一些旋转(0 度或更大),但是在计算在每个轴上的旋转量时涉及到一些非常复杂的数学运算。更容易把这个函数看作是围绕你选择的线旋转某个角度的一种方式。如果您需要同时在几个轴上旋转特定的角度,那么坚持单轴旋转的组合要容易得多。

3D 矩阵变换

正如 CSS 变换的 2D 子集一样,matrix3d()函数允许您在三个轴中的每个轴上组合任意数量的平移、缩放、倾斜和旋转。

我们不会在这里深入讨论 3D 矩阵如何工作的细节,但是函数本身需要 16(!)最终操纵坐标系的各个方面的论据。可以说,它获得了“有史以来最复杂的 CSS 属性”的奖项

就像 2D 版本一样,3D 矩阵不是你通常手写的东西,但它们可以帮助你创建高性能的交互体验,如使用 CSS 和 JavaScript 组合的游戏。例如,本章开头的数字创意指南示例(如图 10-1 所示)大量使用 matrix3d()来计算动画书中所有角色的变换。

摘要

在这一章中,我们开始操纵空间和时间中的元素。我们研究了 2D 或 3D 中的变换如何改变元素的呈现,但不影响页面上的其他元素。我们先睹为快,了解了 matrix()函数和 rotate3d()等高级转换。

将这些与动画放在一起,使用 CSS 过渡或 CSS 关键帧动画,我们可以创建像动画标志一样生动的“喜悦者”,或者像翻转披萨菜单一样更实用的 3D 小部件。

纵观全文,我们已经看到了相应地应用这些效果的技术,尽一切努力不破坏浏览器不支持它们的用户以及使用屏幕阅读器浏览或仅使用键盘导航的用户的体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值