微星-CSS-项目-全-

微星 CSS 项目(全)

原文:Tiny CSS Projects

译者:飞龙

协议:CC BY-NC-SA 4.0

前置内容

前言

学习一门新语言或技能的难点之一是将学到的个别技能应用到我们试图构建的东西中。虽然我们可能了解网格的机制或理解 flex 的工作原理,但学习选择哪种技能和何时(或如何)实现我们设想的特定目标可能会很具挑战性。在这本书中,我们采取了相反的方法,不是从理论开始然后应用到我们的项目中,而是从项目开始,然后看看哪些技能和技术是必要的以实现我们的目标。

但为什么要谈论 CSS 呢?我们可以仅使用浏览器提供的默认设置来编写整个应用程序,但这不会有多少个性,对吧?有了 CSS,我们可以为我们的用户和业务需求实现很多。从品牌识别到使用一致的风格和设计范式引导用户,再到使项目引人注目,CSS 是我们工具箱中的重要工具。

无论是有库、预处理器还是框架,驱动我们应用程序和网站外观的底层技术是 CSS。考虑到这一点,为了避免被库和框架的个别特性和功能所分散注意力,我们选择回归基础,用纯 CSS 编写这本书,因为如果我们理解了 CSS,那么将其应用到任何其他技术栈或环境中都会变得容易得多。

致谢

我们,玛蒂娜和迈克尔,感谢安德鲁·沃尔德伦,收购编辑,以及伊恩·豪,助理收购编辑,他们对我们启动这本书并在整个开发过程中的支持和热情。我们感谢伊丽莎·海德,发展编辑,她从始至终都是我们巨大的支持来源,提供了专业的指导、编辑和鼓励。路易斯·拉扎里斯,技术校对员,以及亚瑟·祖巴列夫,技术发展编辑,提供了深思熟虑、有用的技术反馈和代码审查。感谢你们两位的所有贡献。最后,我们向在整个过程中提供早期访问和审阅的所有读者致以衷心的感谢,他们的反馈帮助塑造和发展了这本书。

我们感谢所有审阅者:Abhijith Nayak,Al Norman,Alain Couniot,Aldo Solis Zenteno,Andy Robinson,Anil Radhakrishna,Anton Rich,Aryan Maurya,Ashley Eatly,Beardsley Ruml,Bruno Sonnino,Carla Butler,Charles Lam,Danilo Zekovic´,Derick Hitchcock,Francesco Argese,Hiroyuki Musha,Humberto A. Sanchez II,James Alonso,James Carella,Jereme Allen,Jeremy Chen,Joel Clermont,Joel Holmes,Jon Riddle,Jonathan Reeves,Jonny Nisbet,Josh Cohen,Kelum Senanayake,Lee Harding,Lin Zhang,Lucian Enache,Marco Carnini,Marc-Oliver Scheele,Margret “Pax” Williams,Matt Deimel,Mladen Ðuric´,Neil Croll,Nick McGinness,Nitin Ainani,Pavel Šimon,Ranjit Sahai,Ricardo Marotti,Rodney Weis,Steffen Gläser,Stephan Max,Steve Grey-Wilson,以及 Vincent Delcoigne。你们的建议帮助使这本书变得更好。

马丁·道登:我要感谢我的家人、朋友和在安德里梅达银河解决方案的同事,他们在我的职业生涯和这本书的写作过程中给予了我坚定不移的支持和鼓励。

我还想感谢 Mozilla 基金会和无数为 MDN 文档做出贡献的个人,他们不辞辛劳地为开发者社区提供了关于 CSS 等网络语言的文档。最后,我要感谢 Caniuse 的创建者 Lennart Schoors 和 Alexis Deveria,以及所有为 Caniuse 做出贡献的人,因为他们使得了解哪些浏览器将支持哪些 CSS 功能变得容易。

迈克尔·吉尔农:这是我写的第一本书,制作它是一个既有趣又具有挑战性的过程。我想感谢所有家庭成员的支持,尤其是我的妻子艾米·史密斯,她在整个过程中一直支持着我。我必须也要特别感谢我的猫, puffin 和 porg,它们试图(但失败了)在书中加入一些奇怪的词。

关于这本书

小型 CSS 项目通过一系列 12 个项目,让设计师和开发者学习 CSS。

适合阅读这本书的人?

小型 CSS 项目适合那些了解 HTML 和前端开发基础知识的读者。不需要 CSS 经验。无论是初学者还是有经验的编码者,通过这本书都能更深入地理解 CSS。本书不是提供 CSS 的理论观点,而是将 CSS 的不同部分应用到项目中,以实践的方式展示 CSS 是如何工作的。

本书组织结构:路线图

本书共有 12 章,每章都是一个独立的项目:

  • 第一章,“CSS 简介”——本章的项目引导读者了解 CSS 的基础知识,检查层叠、特异性和选择器。

  • 第二章,“使用 CSS 网格设计布局”——本章通过为文章设计布局来探索 CSS 网格,在这个过程中,还考察了网格轨道、minmax()、重复函数和分数单位等概念。

  • 第三章,“创建响应式动画加载界面”——本项目使用 CSS 创建一个响应式的动画加载界面,通过可缩放矢量图形和动画来设计 HTML 进度条。

  • 第四章,“创建响应式网络报纸布局”——本章讨论设计多列响应式网络报纸布局。它探讨了 CSS 多列布局模块、计数器样式、损坏的图像,以及如何通过使用媒体查询来调整布局。

  • 第五章,“具有悬停交互的总结卡片”——本项目通过使用背景图像、悬停时显示内容的过渡效果以及媒体查询来检查能力和浏览器窗口大小,创建了一系列卡片。

  • 第六章,“创建个人资料卡片”——本章的项目创建了一个个人资料卡片,并探讨了自定义属性、背景渐变,以及设置图像大小和使用 Flexbox 进行布局。

  • 第七章,“充分利用浮动”——本章展示了 CSS 浮动在定位图片、围绕 CSS 形状排列内容以及创建首字母下沉效果方面的强大功能。

  • 第八章,“设计结账购物车”——本章讲述了设计结账购物车,这涉及到样式化响应式表格、使用 CSS 网格进行布局、格式化数字以及通过使用媒体查询根据视口大小条件性地设置 CSS。

  • 第九章,“创建虚拟信用卡”——本章重点介绍创建虚拟信用卡,并通过在悬停时翻转卡片来实现 3D 效果。

  • 第十章,“样式化表单”——本章探讨了设计表单,包括单选按钮、输入框和下拉菜单,以及提高可访问性。

  • 第十一章,“动画社交媒体分享链接”——这个项目使用 CSS 过渡来动画化社交媒体分享链接,并探讨了 CSS 架构选项,如 OOCSS、SMACSS 和 BEM。

  • 第十二章,“使用预处理器”——最后一章展示了我们在编写 CSS 时如何使用预处理器,并介绍了 Sass 语法。

关于代码

本书包含许多源代码示例,无论是编号列表还是与普通文本内联。在两种情况下,源代码都使用固定宽度字体如这样来格式化,以将其与普通文本区分开来。有时代码也会**加粗**以突出显示章节中先前步骤的变化,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已被重新格式化;我们添加了换行并重新调整了缩进,以适应书籍中的可用页面空间。在某些情况下,即使这样也不够,列表中还包括了行续接标记(➥)。许多列表旁边都有代码注释,突出显示重要概念。

您可以从本书的 liveBook(在线)版本中获取可执行的代码片段livebook.manning.com/book/tiny-css-projects。书中示例的完整代码可以从 Manning 网站www.manning.com和 GitHubgithub.com/michaelgearon/Tiny-CSS-Projects下载。

liveBook 讨论论坛

购买 Tiny CSS Projects 包括免费访问 liveBook,Manning 的在线阅读平台。使用 liveBook 的独家讨论功能,您可以在全球范围内或针对特定部分或段落附加评论。为自己做笔记、提问和回答技术问题以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问livebook.manning.com/book/tiny-css-projects/discussion。您还可以在livebook.manning.com/discussion了解更多关于 Manning 论坛和行为准则的信息。

曼宁对读者的承诺是提供一个场所,让读者之间以及读者和作者之间可以进行有意义的对话。这并不是对作者参与特定数量承诺的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向他们提出一些挑战性的问题,以免他们的兴趣转移!只要这本书有售,论坛和先前讨论的存档将可通过出版社的网站访问。

其他在线资源

通常,我们无法记住一个属性是如何工作的,或者我们有哪些可用的值。查找特定属性、函数或值的工作方式的一个极好资源是 MDN 文档 (developer.mozilla.org/en-US)。

尽管 CSS 功能中的任何特定方面都可以在 CSS 规范中定义,但这并不意味着所有浏览器都支持它。我们经常发现自己需要了解哪些浏览器支持什么,以及我们是否应该创建回退或使用替代方法来实现我们的目标。Caniuse (caniuse.com) 是一个极好的资源,它允许我们检查特定的属性或函数,以查看它在各个浏览器版本中的支持程度如何。

最后,为了确保每个人都能访问和使用我们的网站和应用,我们不能忘记无障碍的重要性。万维网联盟的 Web 无障碍倡议提供的文档是开始的好地方,它们链接到许多其他资源,包括网络内容无障碍指南 (www.w3.org/WAI/fundamentals)。

关于作者

Dowden 作者

Martine Dowden 是一位作家、国际演讲者和 Andromeda Galactic Solutions 获奖首席技术官。她的专业知识包括心理学、设计、艺术、无障碍、教育、咨询和软件开发。"Tiny CSS Projects"是她关于网络技术的第四本书,汇集了她在构建美观、功能和无障碍网络界面方面的 15 年经验。对于她的社区贡献,Martine 被命名为微软开发者技术 MVP 和谷歌网络技术及 Angular 开发者专家。

Gearon 作者

Michael Gearon 是一位来自英国威尔士的用户体验设计师和前端开发者。他在南威尔士大学学习媒体技术并获得学士学位的同时,练习编码和设计。从那时起,Mike 与知名的英国品牌合作,包括 Go.Compare 和 Ageas。他现在在公务员部门工作,之前在公司注册处工作,目前在国家数字服务处工作。

关于封面插图

《Tiny CSS Projects》封面上的图像标题为“M’de. de bouquets à Vienne”,或“维也纳的花贩”,这幅画取自雅克·格拉塞·德·圣索沃尔的作品集,该作品集于 1797 年出版。每一幅插图都是手工精细绘制和着色的。

在那些日子里,仅凭人们的服饰就能轻易地识别出他们居住的地方以及他们的职业或社会地位。曼宁通过基于几个世纪前丰富多样的地域文化的书封面来庆祝计算机行业的创新精神和主动性,这些文化通过像这一系列这样的图片被重新带回生活。

1 CSS 简介

本章涵盖

  • CSS 的简要概述

  • 基本 CSS 样式

  • 如何有效地选择 HTML 元素

层叠样式表(CSS)用于控制网页元素的显示。CSS 使用样式规则来指导浏览器选择某些元素并对它们应用样式和效果。

如果你刚开始学习 CSS 或需要复习,第一章是一个很好的起点。我们将从 CSS 的简要历史开始,迅速过渡到如何开始使用 CSS,探讨将 CSS 与 HTML 链接的方法。

当我们的 CSS 运行起来后,我们将通过创建一个静态的单列文章页面,包含基本媒体组件(如标题、内容和图像)来查看 CSS 的结构,以了解所有元素是如何协同工作的。

1.1 CSS 概述

Håkon Wium Lie 在 1994 年提出了 CSS 的概念,这比 Tim Berners-Lee 在 1990 年创建 HTML 晚了几年。CSS 通过颜色、布局和字体排印等选项将样式从网页内容中分离出来。

1.1.1 关注点分离

这种内容和呈现的分离基于设计原则“关注点分离”(SoC)。这一原则背后的理念是,计算机程序或应用应该被分解成单个、独立的、按目的分离的部分。保持良好的 SoC 的好处包括

  • 减少代码重复,因此更容易维护

  • 可扩展性,因为它要求元素专注于单一目的

  • 稳定性,因为代码更容易维护和测试

基于这一原则,HTML 充当网页的结构和内容,CSS 是呈现,JavaScript(JS)提供额外的功能。它们共同构成了网页。图 1.1 显示了这一过程的示意图。

图 1.1 网页的分解

自从 2000 年代中期智能手机的引入以来,网络已经扩展到移动网站(通常使用 m.子域名,如 m.mywebsite.com),这些网站通常比桌面版本功能更少,并且采用了响应式和自适应设计。创建响应式/自适应或针对移动设备的网站有其优点和缺点。

响应式设计和自适应设计之间的区别

响应式设计使用单一流动布局,可以根据屏幕大小、方向和设备偏好等因素进行变化。自适应设计也可以根据这些因素进行变化。但与单一流动布局不同,我们可以创建多个固定布局,这使我们能够对每个布局有更大的控制权——但代价是比单一响应式布局需要更多的时间。在实践中,我们可以将这两种方法结合起来使用。

通常情况下,响应式和自适应设计是行业发展的趋势,尤其是在 CSS 不断扩展,为我们提供了更多基于窗口大小和媒体类型(如屏幕或打印)应用 CSS 的能力之后。自从 1994 年 CSS 发布以来,共有三个主要版本发布:

  • 1996 年—第一个万维网联盟(W3C)的 CSS 推荐

  • 1997 年—CSS2 的第一个工作草案

  • 1999 年—第一个三个 CSS3 草案(颜色配置文件、多列布局和分页媒体;www.w3.org/Style/CSS20

1999 年之后,发布策略改为允许更快、更频繁地发布新功能。现在 CSS 被划分为模块,以 1 开始编号,随着功能和功能的演变和扩展,向上递增。

CSS 级别 1 模块是 CSS 中全新的东西,例如一个以前作为官方标准不存在的属性。经过几个版本讨论的模块,如媒体查询、颜色、字体以及层叠和继承模块,有更高的级别编号。

将 CSS 划分为模块的好处是每个部分可以独立移动,而不需要整个语言的大规模更改。已经有一些关于是否需要有人宣布当前阶段为 CSS4 的讨论,即使只是为了承认自 1999 年以来 CSS 已经发生了很大变化。然而,这个想法至今还没有得到任何支持。

1.1.2 什么是 CSS?

CSS 是一种声明性编程语言:代码告诉浏览器需要做什么,而不是如何做。例如,我们的代码表示我们想要某个标题是红色,浏览器将决定如何应用这种样式。这很有用,因为如果我们想增加段落的行高以改善阅读体验,那么布局、尺寸和格式化新行高的决定权在浏览器手中,这减少了开发者的工作量。

领域特定语言

CSS 是一种领域特定语言(DSL)——一种为解决特定问题而创建的专用语言。DSL 通常比通用语言(如 Java 和 C#)简单。CSS 的特定目的是样式化网页内容。SQL、HTML 和 XPath 等语言也是 DSL。

自从 1994 年以来,CSS 已经走了很长的路。现在我们有方法来动画和过渡元素,创建运动路径来动画可缩放矢量图形(SVG)图像,并根据视口大小条件应用样式。这类功能以前只能通过 JavaScript 或 Adobe Flash(现已退役)来实现。我们可以通过查看 CSS Zen Garden([www.csszengarden.com](http://www.csszengarden.com/))来一窥可能性;通过比较第一个和最后一个设计,我们可以观察到 CSS 随时间的发展(www.w3.org/Style/CSS20)。

在过去,使用透明度、圆角、遮罩和混合等设计选择是可能的,但需要非传统的 CSS 技术和技巧。随着 CSS 的发展,添加了属性来用标准、文档化的功能替换这些技巧。

CSS 预处理器

CSS 的演变也导致了 CSS 预处理器和 Syntactically Awesome Style Sheets (Sass)的引入,Sass 于 2006 年发布。它们被创建出来是为了简化代码的编写,使其更易于阅读和维护,同时也提供了 CSS 本身不具备的附加功能。在第十二章中,我们将使用预处理器来样式化一个页面。

可以说,CSS 正处于一个黄金时代。随着语言的持续发展,新的和创造性的体验机会几乎是无限的。

1.2 通过创建文章布局开始学习 CSS

在我们的第一个项目中,我们将探索网络上的一个常见用例:创建单列文章。本章重点介绍如何将 CSS 链接到 HTML,并探讨我们可以使用的选择器来样式化我们的 HTML。

我们首先需要理解的是如何将我们的 CSS 与 HTML 关联起来,以及如何选择一个元素。然后我们再考虑我们想要应用哪些属性和值。让我们先从一些基础知识开始。

如果你刚开始学习编程,你通常可以找到免费工具来使用这些项目。你有在线编码的选项,或者你可以在电脑上使用代码编辑器,如 Sublime Text (www.sublimetext.com)、Brackets ( brackets.io) 或 Visual Studio Code (code.visualstudio.com) 进行工作。或者,你也可以使用 Mac 的 TextEdit (mng.bz/rd9x)、Windows 的记事本 (mng.bz/VpAN) 或 Linux 的 gedit (wiki.gnome.org/Apps/Gedit) 作为基本文本编辑器。

与使用代码编辑器或集成开发环境(IDE)相比,使用基本文本编辑器的缺点是它缺乏语法高亮。这种高亮会根据文本在代码中的作用以不同的颜色和字体显示文本,这有助于提高可读性。

你还可以使用免费的在线开发编辑器,如 CodePen (codepen.io)。在线开发编辑器是测试想法的好方法;它们为前端项目提供了快速、便捷的访问。CodePen 提供了一个付费的 Pro 选项,允许你托管图像等资产,这些资产你将在后面的章节中需要。另一个选项是链接到存储图像的 GitHub 位置,因为所有上传到 GitHub 的资产都存储在raw.githubusercontent.com域名下。

当你在电脑上安装了代码编辑器或者选择了在线编辑器并创建了账户后,你需要获取本章的起始代码。我们在 GitHub (github.com/michaelgearon/Tiny-CSS-Projects) 上创建了一个代码仓库,其中包含了你需要跟随每个章节的所有代码。图 1.2 显示了该仓库的截图。

图 1.2 GitHub 上的 Tiny-CSS-Projects 仓库

代码按章节组织在文件夹中。每个章节文件夹中都有两个版本的代码:

  • before—包含项目的初始代码。如果你要和本章一起编码,你需要这个版本。

  • after—包含章节末尾完成的项目的最终状态,并应用了展示的 CSS。

使用屏幕顶部的代码下拉菜单下载(或者如果你熟悉 Git,克隆)项目。如果你要和本章一起编码,从第一章的before文件夹中获取文件,并将它们复制到你的项目文件夹或笔中。你应该会看到一个包含一些初始代码和一个空 CSS 文件的 HTML 文件。如果你在浏览器中打开 HTML 文件或将<body>标签的内容复制到 CodePen 中,你会看到内容没有样式,除了浏览器提供的默认样式(图 1.3)。现在你就可以开始用 CSS 样式化内容了,如列表 1.1 所示。

图 1.3 我们文章的初始 HTML

注意:CodePen 会自动处理<head>标签中的信息。因此,如果你在 CodePen 或类似的在线编辑器中跟随,你只需要复制<body>标签内的代码。

列表 1.1 开始的 HTML

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Chapter 1 - CSS introduction</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <img src="sample-image.svg" width="100" height="75" alt="">
  <article>
    <header>
      <h1>Title of our article (heading 1)</h1>
      <p>
        Posted on
        <time datetime="2015-05-16 19:00">May 16</time>
        by Lisa.
      </p>
    </header>
    <p>Lorem ipsum dolor sit amet, ...</p>
    <ol class="ordered-list">
      <li>List item 1
        <ul>
          <li>Nested item 1</li>
          <li>Nested item 2</li>
        </ul>
      </li>
      <li>List item 2</li>
      <li>List item 3</li>
      <li>List item 4</li>
    </ol>
    <img src="sample-image.svg" width="200" height="150" alt="">
    <p>Curabitur id augue nulla ...</p>
    <blockquote id="quote-by-author">
     Nunc eleifend nulla lobortis ...
    </blockquote>
    <p>Etiam tempor vulputate varius ...</p>
    <h2>Heading 2</h2>
    <p>
       In ac euismod tortor ...
       <a target="_blank" href="#">In eleifend in dolor id aliquet</a>
       ...
    </p>
    <p>In id lobortis leo ...</p>
    <img src="sample-image.svg" width="200" height="150" alt="">
    <h3>Heading 3</h3>
    <p>
      Mauris sit amet tempor ex ...
      <a href="#">Sed vulputate eget ante vel vehicula</a>.
      Curabitur ac velit sed ...
    </p>
    <p>Quisque vel erat et ...</p>
    <h4 class="small-heading">Heading 4</h4>
    <p>Aliquam porttitor, ex ...
      <a href="#">Cras sed finibus libero</a>
      Duis lobortis, ipsum ut consectetur ...
    </p>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <svg  width="300" height="150">
      <circle cx="70" cy="70" r="50"></circle>
      <rect y="80" x="200" width="50" height="50" />
    </svg>
    <h4>Heading 4</h4>
    <h5 class="small-heading">Heading 5</h5>
    <p>In finibus ultrices nulla ut rhoncus ...</p>
    <h6 class="small-heading">Heading 6</h6>
    <p lang="it">Questo paragrafo è definito in italiano.</p>
    <ul class="list">
      <li>List item 1
        <ul>
          <li>Nested item 1</li>
          <li>Nested item 2</li>
        </ul>
      </li>
      <li>List item 2</li>
      <li>List item 3</li>
      <li>List item 4</li>
    </ul>
    <footer>
      <p>Footer text</p>
    </footer>
  </article>
  <p>Nam rutrum nunc at lectus ...</p>
</body>
</html>

1.3 向我们的 HTML 添加 CSS

当我们用 CSS 进行样式设计时,我们有三种方法将 CSS 应用到我们的 HTML 中:

  • 内联

  • 内嵌

  • 外部

1.3.1 内联 CSS

我们可以通过给一个元素添加一个style属性来内联 CSS。这种方法让我们直接在 HTML 中向元素添加 CSS。

属性总是在开标签中指定,通常由属性的名称组成——在这个例子中是style。属性有时后面跟着一个等号(=)和用引号括起来的值。所有的 CSS 都放在开引号和闭引号之间。

例如,让我们将我们的heading的颜色设置为crimson<h1 style="color: crimson">文章的标题(heading 1)</h1>。如果我们保存我们的 HTML 并在浏览器中查看,我们会看到它是 crimson 色。如果我们使用的是代码编辑器而不是网络客户端(CodePen),我们需要刷新浏览器页面来查看我们的更改。图 1.4 显示了输出。注意,唯一受影响的元素是我们应用了样式的<h1>

图 1.4 紫红色标题

内联 CSS 的一个缺点是它在 CSS 中具有最高的特定性,我们将在稍后更详细地探讨这一点。内联 CSS 的另一个主要缺点是它很快就会变得难以管理。假设我们在一个 HTML 文档中有 20 个段落。我们需要应用相同的样式属性和相同的 CSS 属性 20 次,以确保所有段落看起来都一样。这种情况涉及两个问题:

  • 我们的关注点不再分离。负责内容的 HTML 和负责样式的 CSS 现在在同一个地方,并且紧密耦合。

  • 我们在许多地方重复代码,这使得维护和保持样式一致性变得极其困难。

内联 CSS 的优点是页面加载性能。浏览器首先加载 HTML 文件,然后加载它渲染页面所需的任何其他文件。当 CSS 已经在 HTML 文件中时,浏览器不需要等待从单独的位置加载它。让我们撤销对<h1>添加的样式,并看看一种具有与内联相同优点但缺点更少的不同的技术。

1.3.2 嵌入式 CSS

要解决重复代码的问题,我们可以在嵌入式(有时称为内部)<style>元素中添加我们的 CSS。<style>元素必须放置在开<head>标签和闭<head>标签之间。要使所有标题元素变为深红色,我们可以使用以下列表中的代码片段。

列表 1.2 嵌入式 CSS

<!DOCTYPE html>
<html lang="en">
 <head>
   ...
  <style>
   h1, h2, h3, h4, h5, h6 {
    color: crimson;
   }
  </style>
 </head>
 <body>
  ...
 </body>
</html>

这种方法的优点是现在我们将所有 CSS 组合在一起,CSS 将应用于整个 HTML 文档。在我们的例子中,该网页内的所有标题(<h1><h2><h3><h4><h5><h6>)都将变为深红色,正如我们可以在图 1.5 中观察到的。

图 1.5 应用到所有标题的样式

我们还看到嵌入式 CSS 与内联 CSS 在书写方式上的差异。当我们编写嵌入式 CSS 时,我们创建所谓的规则集,它由图 1.6 中显示的部分组成。

图 1.6 CSS 规则的示例

规则中定义应用于哪些元素的部分被称为选择器。图 1.6 中的规则将应用于所有<h1>元素;其选择器为h1

要应用多个选择器,我们将它们作为逗号分隔的列表写在开括号之前。例如,要选择所有<h1><h2>元素,我们会写h1,h2 { ... }

声明由属性组成——在本例中为color,后跟一个冒号,然后是属性值(red)。声明定义了所选元素将被如何样式化。属性和值都必须用美式英语书写。如colourcapitalise之类的拼写变体不受支持,并且浏览器不会识别。当浏览器遇到无效的 CSS 时,它会忽略它。如果一个规则中有无效的声明,则有效的声明仍然会被应用;只有无效的声明会被忽略。

嵌入式 CSS 适用于一次性的网页,其中样式特定于该页面。它很好地组织了 CSS,允许我们编写应用于元素的规则,从而避免在多个地方复制和粘贴相同的样式。它还具有与内联样式相同的性能优势,即浏览器可以立即访问 CSS;它不需要等待从不同位置获取 CSS。

将 CSS 放在 HTML 文档中的缺点是,CSS 只适用于该文档。所以如果我们的网站有多个页面,这通常是情况,我们就需要将 CSS 复制到每个 HTML 文档中。除非这些样式是由模板或后端语言(如 PHP)生成的,否则这项任务将很快变得难以维护,尤其是对于大型应用程序,如博客和电子商务网站。接下来,让我们最后一次撤销项目中的更改,并查看第三种技术。

1.3.3 外部 CSS

与内联 CSS 类似,外部 CSS 方法将我们的样式分组在一起,但它将 CSS 放在单独的 .css 文件中。通过将我们的 HTML 和 CSS 分离,我们可以有效地分离我们的关注点:内容和样式。

我们通过使用 <link> HTML 标签将样式表链接到 HTML。链接元素需要两个属性来指定样式表:rel 属性,它描述了 HTML 文档与被链接对象之间的关系,以及 href 属性,它代表 超文本引用 并指示要包含的文档的位置。以下列表显示了我们将样式表链接到我们的 HTML 的方法。

列表 1.3 将外部 CSS 应用到 HTML

<!DOCTYPE html>
<html>
<head>
   <link rel="stylesheet" href="styles.css">
 </head>
 <body>
   <h1>Inline CSS</h1>
 </body>
</html>

大多数情况下,这种方法是我们在互联网上看到的方法,因此我们将在这本书中一直使用这种方法。外部样式表的好处是我们的 CSS 在一个单独的文档中,可以一次性修改以应用到所有 HTML 页面上。这种方法的缺点是它需要浏览器额外的请求来检索该文档,从而失去了将 CSS 直接放在 HTML 中的性能优势。

1.4 CSS 的层叠

CSS 的一个基本特性是我们需要理解的是层叠。当 CSS 被创建时,它是围绕 层叠 的概念开发的,这允许样式相互覆盖或继承。这个概念为多个样式表竞争网页的展示铺平了道路。

因此,当我们使用浏览器的开发者工具检查一个元素时,我们有时会看到多个 CSS 值在争夺浏览器渲染的机会。浏览器通过特定性来决定将哪些 CSS 属性值应用到元素上。特定性允许浏览器(或用户代理)确定哪些声明与 HTML 相关,并将样式应用到该元素。

特定性计算的一个方面是样式表的应用顺序。当应用多个样式表时,后续样式表中的样式将覆盖先前样式表提供的样式。换句话说,假设使用了相同的选择器,最后声明的样式将获胜。CSS 有三种不同的样式表来源:

  • 用户代理样式表

  • 作者样式表

  • 用户样式表

1.4.1 用户代理样式表

第一个来源是浏览器的默认样式。当我们打开项目,在我们添加任何样式之前,我们的元素并不都看起来一样。例如,我们的标题比我们的文本更大、更粗。这种格式化是由用户代理(UA)样式表定义的。这些样式表在三种类型中具有最低的优先级,我们发现不同的浏览器对 HTML 属性的呈现略有不同。

大多数情况下,用户代理(UA)样式表设置字体大小、边框样式以及一些基本布局,例如文本输入和进度条,这在用户样式表找不到或发生文件加载错误时可能很有用。UA 样式表提供了一些回退样式,这使得页面更易于阅读,并保持了不同元素类型之间的视觉差异。

1.4.2 作者样式表

我们开发者编写的样式表被称为作者样式表,在浏览器显示的样式优先级中通常具有第二高的优先级。当我们创建网页时,我们编写的 CSS(内嵌、外部或内联)并将其应用于我们的网页的样式表就是作者样式表。

1.4.3 用户样式表

访问我们网页的用户可以使用他们自己的样式表来覆盖作者和 UA 样式。这个选项可以改善他们的体验,特别是对于残障用户。

用户可能出于各种原因使用自己的样式表,例如设置最小字体大小、选择自定义字体、提高对比度或增加元素之间的间距。任何用户都可以将用户样式表应用于网页。这些样式表如何应用于网页取决于浏览器,通常是通过浏览器设置或插件。

用户样式表仅适用于添加它的用户,并且仅在应用它的浏览器中生效。这种更改是否可以从一个设备传输到另一个设备,取决于浏览器本身及其在多个设备之间同步用户设置和已安装插件的能力。

1.4.4 CSS 重置

浏览器提供的默认样式并不一致。每个浏览器都有自己的样式表。例如,默认样式在 Google Chrome 中与在 Apple 的 Safari 中不同。如果我们希望我们的应用程序在所有浏览器中看起来都一样,这种差异可能会带来一些挑战。

幸运的是,有两种选择可用:CSS 重置和 CSS 规范化器(例如 Normalize.css;github.com/necolas/normalize.css)。尽管两者都可以用来解决跨浏览器样式问题,但它们的工作方式截然不同。

通过使用 CSS 重置,我们撤销了浏览器的默认样式;我们告诉浏览器我们根本不想要任何默认样式。在没有应用任何作者样式的情况下,所有元素,无论它们是什么,看起来都像普通文本(图 1.7)。

图片

图 1.7 CSS 重置应用效果

要将 CSS 重置应用于我们的项目,首先我们创建一个重置样式表并将其添加到我们的项目中。在我们的项目文件夹中,我们创建一个名为reset.css的文件。然后我们将重置 CSS 复制到文件中。存在许多重置选项;一个常用的选项可以在meyerweb.com/eric/tools/css/reset找到。

最后,我们需要将我们的样式表链接到我们的 HTML 文件。因为顺序很重要,我们想要确保在我们的<head>中包含重置 CSS,在作者样式之前。因此,我们的 HTML 将看起来像列表 1.4。

页面加载性能

为了可读性,将重置和我们的样式放在单独的文件中比将所有内容放在一个文件中要好得多。然而,这种方法对页面加载性能来说并不理想。

在生产环境中,我们想要做以下事情之一:

  • 将重置 CSS 放置在我们自己的样式所在的同一文件的开始处,这样我们只加载一个样式表。我们可以手动做这件事,或者将其作为构建过程的一部分。

  • 在我们自己的样式之前从内容分发网络(CDN)加载重置代码。通过从 CDN 加载,我们增加了用户在他们机器上已经缓存了代码的可能性。

列表 1.4 添加 CSS 重置

<head>
  ...
  <link rel="stylesheet" href="reset.css">      ①
  <link rel="stylesheet" href="styles.css">     ②
 </head>

① 重置样式表

② 作者样式表

CSS 重置的好处是我们有一个空白画布可以从中开始。如图 1.7 所示,现在我们的所有元素看起来都像是普通文本。缺点是我们需要为所有元素定义基本样式,包括为列表添加项目符号和区分标题级别。此外,每个 CSS 重置的版本都会根据版本和编写它的开发者有所不同。

我们的另一种选择是使用标准化器。与重置样式不同,标准化器专门针对在不同浏览器中具有差异的元素,并应用规则以标准化它们。

1.4.5 标准化器

就像 CSS 重置一样,标准化器根据版本和作者的不同,对样式进行轻微的调整。一个常用的 CSS 标准化器可以在necolas.github.io/normalize.css找到。我们可以以与 CSS 重置代码相同的方式将其应用于我们的项目:创建一个文件,将代码复制到文件中,并将其链接到我们的 HTML 文件。请注意,同样的性能考虑也适用于这里。

当应用标准化器(如图 1.8 所示)时,我们的 HTML 看起来与最初相同,因为它处理的大多数差异都在这个特定项目中未使用的元素上。根据我们使用的浏览器,我们可能会注意到<h1>的大小有所不同。

图片

图 1.8 应用到我们项目中的标准化器

好消息是,用户代理(UA)样式表差异比 10 多年前的问题要小得多。今天,浏览器在样式上更加一致,因此使用 CSS 重置或标准化器更多的是一种个人选择,而不是必需品。

然而,仍然存在一些差异。无论我们是否使用 CSS 重置或正常化器,我们都应该在各种设备和浏览器上测试我们的代码。

1.4.6 !important 注释

!important注释你可能在一些样式表中见过。通常在所有其他方法都失败时作为最后的手段使用,它是一种覆盖特定性并声明特定值是最重要的事情的方法。然而,权力越大,责任越大。!important注释最初被创建为一个可访问性功能。

记得我们提到过用户能够应用自己的样式以获得更好的用户体验吗?这个注释是为了帮助用户定义自己的样式,而无需担心特定性。因为它覆盖了任何其他样式,所以它确保了用户的样式总是具有最高的优先级,因此是应用的那一个。

使用!important被认为是不良实践,因此我们应该尽量避免在我们的作者样式表中使用它。此外,这个注释打破了 CSS 的自然级联,可能会使管理样式表变得更加困难。

1.5 CSS 中的特定性

当多个属性值被应用于一个元素时,其中一个将胜过其他。我们通过一个多步骤的过程来确定胜者。我们暂时忽略!important(第 1.4.6 节),因为它打破了正常的流程;我们稍后会回到它。

首先,我们来看一下值从哪里来。在规则中明确定义的任何内容都将覆盖继承的值。例如,在列表 1.5 和 1.6 中,如果我们将<body>元素的字体颜色设置为red,那么<body>内部的所有元素都将显示红色文本。

字体颜色被子元素继承。如果我们特别在<body>内的段落中设置不同的颜色,继承的red值将被在段落上设置的更具体的blue值覆盖。因此,那个段落的文本颜色将是蓝色。

列表 1.5 继承示例(HTML)

<body>
  <h1>Example</h1>       ①
  <p>My paragraph</p>    ②
</body>

① 我们的页眉将继承红色颜色。

② 段落的颜色将是蓝色,这是由段落规则设置的。

列表 1.6 继承示例(CSS)

body { color: red }
p { color: blue }

并非所有属性值都会被继承。与主题相关的样式,如颜色和字体大小通常会被继承;布局考虑通常不会。这个指南是松散的,有明确的例外,但这是一个好的起点。我们将在整个项目中根据具体情况处理例外。

如果属性值没有被继承,浏览器会查看所使用的选择器类型,并通过数学计算特定性值。我们将在第 1.6 节中更详细地介绍每种选择器类型是什么,但首先让我们看看数学是如何应用的。

浏览器查看选择器,根据规则分类使用的选择器类型,并应用类型值。然后它添加所有值并得到一个最终特定性值。图 1.9 展示了这个过程。最大的数字获胜,所以图中的规则 1 会胜过规则 2。

图 1.9 计算特定性

根据选择器类型的特定性值如下:

  • 100—ID 选择器

  • 10—类选择器、属性选择器和伪类

  • 1—类型选择器和伪元素

  • 0—通用选择器

如果我们仍然有平局,浏览器会查看样式来自哪个样式表。如果两个值都来自同一个样式表,文档中较后的那个获胜。如果值来自不同的样式表,顺序如下:

  1. 用户样式表

  2. 作者样式表(按照导入的顺序;最后一个生效)

  3. UA 样式表

我们之前将!important放在一边。现在我们理解了正常流程,让我们将其添加回混合中。当一个值有!important注释时,流程被短路,带有注释的值自动获胜。

如果两个值都有!important注释,浏览器遵循正常流程。图 1.10 显示了通过样式表的流程,包括!important声明。

图 1.10 CSS 优先级顺序

我们已经确定选择器的类型会影响特定性。让我们更仔细地查看选择器,并在我们的项目中使用它们。

1.6 CSS 选择器

选择器设置我们想要定位的 HTML 元素。在 CSS 中,我们有七种方式来定位我们想要样式的 HTML 元素,如以下各节所述。

1.6.1 基本选择器

将样式应用于 HTML 元素的最常见方法是根据名称、ID 或类名选择它们。这些最常使用,因为它们与 HTML 元素本身或元素上设置的属性的一对一映射。

类型选择器

类型选择器通过名称定位 HTML 元素。使用类型选择器的优点是,当我们阅读我们的 CSS 时,我们可以快速确定如果我们在规则中做出更改,哪些 HTML 元素会受到影响的。这个选择器不需要我们在 HTML 中添加任何特定的标记来定位元素。

让我们使用类型选择器来定位所有我们的标题(<h1><h6>),并将它们的颜色改为深红。我们的 CSS 将是h1, h2, h3, h4, h5, h6 { color: crimson; }。图 1.11 显示我们的标题颜色已更改。

图 1.11 标题颜色更改

类选择器

我们可以在我们想要的任何不同元素上使用类选择器。通过将类名应用于元素,我们正在将多个 HTML 元素分组,以便当我们应用样式时,它们将扩展到具有该类名的任何元素。

要向 HTML 添加类,我们使用 class 属性。在 class 属性内,我们可以添加尽可能多的值(或类),每个值用空格分隔。

我们有许多方法和方式来编写我们的类名,例如块状、元素、修饰符(BEM)方法(en.bem.info)和可伸缩和模块化 CSS 架构(SMACSS;smacss.com),这些都是编写一致样式表的指南。

主要目的是编写对每个人都有意义的类名。例如,给段落元素添加类名 text 会非常令人困惑。其他元素,如我们的标题,也可以被视为文本,所以可能不清楚我们指的是哪个特定元素。

根据特定的样式(如 color)应用类名也可能很危险。给一个元素添加类名 blue 可能会立即生效,但如果设计改变并且应用的颜色现在是红色,我们的类名将不再有意义。

在我们的 HTML 中,我们发现一些标题具有 small-heading 类。我们将创建一个选择 small-heading 并将元素的文本改为大写的规则。

要选择 small-heading 类名,在 CSS 中我们首先输入点(.)然后是类名 small-heading。然后我们的样式进入大括号,如下所示:.small-heading { text-transform: uppercase }。图 1.12 展示了我们的大写标题。注意,其他标题没有受到影响——只有那些应用了该类的标题。

图 1.12 类选择器应用于具有类名 small-heading 的元素

ID 选择器

在 HTML 中,ID 是唯一的。任何给定的 ID 应该在网页上只使用一次。如果 ID 重复,我们的代码被认为是无效的 HTML。

通常我们应该避免使用 ID 选择器;因为它们在 HTML 中必须是唯一的,针对 ID 构造的规则不容易重用。此外,ID 选择器是可用的最具体选择器之一,使得使用 ID 选择器应用的风格难以覆盖。除非元素的唯一性是关键,否则请避免使用 ID 属性。

我们的示例文章包含一个具有 ID 属性的 blockquote,其值为 quote-by-author。在我们的 CSS 中,要选择 blockquote,我们使用井号(#),然后紧接着是我们想要定位的 ID。然后我们有大括号,在大括号内放置我们的声明,如下所示。

列表 1.7 ID 选择器

#quote-by-author {
  background: lightgrey;
  padding: 10px;
  line-height: 1.75;
}

图 1.13 展示了应用于我们项目的代码。

图 1.13 显示了 #quote-by-author 应用的样式

1.6.2 组合器

另一种编写 CSS 的方法是使用 组合器,这允许在不过度使用 classID 名称的情况下编写更复杂的 CSS。这里有四种组合器:

  • 后代组合器(space

  • 子组合器(>

  • 相邻兄弟组合器(+

  • 通用兄弟组合器(~

需要理解的一个重要概念是元素之间的关系。在接下来的几个示例中,我们将探讨如何使用元素之间的关系来定位不同的 HTML 元素以样式化我们的文章。图 1.14 介绍了我们将要检查的关系类型。

图 1.14 HTML 元素之间的关系

后代组合符(空格)

使用后代组合符的选择器选择父元素内的所有 HTML 元素。使用后代组合符的选择器由三部分组成。第一部分是父元素,在本例中是文章元素。父元素后面跟着一个空格,然后是我们想要选择的所有元素。图 1.15 示例了语法。

图 1.15 使用后代组合符的选择器示例

在此示例中,浏览器将找到任何 <article> 元素,针对其父 <article> 元素中的所有后代段落 (<p>) 进行双倍行距处理。当应用此选择器时,我们的文章看起来像图 1.16。

图 1.16 子段落双倍行距

子组合符(>)

子组合符允许我们定位特定选择器的直接子元素。这个组合符与使用后代组合符的选择器不同,因为在子组合符的情况下,目标元素必须是直接子元素。使用后代组合符的选择器可以选中任何后代(子元素、孙子元素、曾孙元素等)。

在我们的项目中,我们将对文章中的列表项进行样式化。如列表 1.8 所示,我们有一个无序列表 (<ul>) 和列表项 (<li>)。第一个子元素有自己的嵌套项,这些将是孙子元素和曾孙元素。

列表 1.8 HTML 列表项

<ul class="list">                      ①
  <li>List item 1                      ②
    <ul>                               ③
      <li>Nested item 1</li>           ④
      <li>Nested item 2</li>           ④
    </ul>
  </li>
  <li>List item 2</li>                 ②
  <li>List item 3</li>                 ②
  <li>List item 4</li>                 ②
 </ul>                                 ①

① 父元素项 (.list)

② .list 的子元素

③ .list 的曾孙元素

④ .list 的曾孙元素

我们将仅对第一级列表项——即 <ul> 的直接子元素——使用包含 listclass 属性值进行样式化,使其呈现深红色,而不会影响嵌套列表项(即曾孙元素)。因此,浏览器将找到包含 list 类的元素,仅针对它们的直接子元素(即列表项 <li>)更改 colorcrimson。我们将使用以下 CSS:

.list > li { color: crimson; }

使用此 CSS,整个列表变为深红色,而不仅仅是顶级列表项。颜色应用于 <li> 元素及其所有后代。尽管我们选择了直接子元素,但由于颜色是继承的,子元素也变为深红色。

因此,为了仅选择顶级元素,我们需要添加第二个规则

.list > li ul { color: initial }

这将使嵌套列表项恢复到初始颜色,如图 1.17 所示。

图 1.17 将子组合符应用于 list

我们可以反过来执行这个操作,选择子元素的父母元素,对吧?简短的回答是:不行,以下示例不会起作用:article < p { color: blue; }。如果我们想选择一个元素的父元素或祖先元素,我们需要使用has()伪类——article:has(p) { color: blue; }——这在 1.6.3 节中有介绍。

相邻兄弟组合符(+)

当我们需要对与另一个元素处于同一级别的元素进行样式设计时,就像你的兄弟姐妹与你处于家族树中的同一级别一样,我们可以使用相邻兄弟组合符。如果我们想定位直接跟在另一个元素后面的元素,我们可以使用一个使用相邻兄弟组合符的选择器。

在列表 1.9 中,浏览器将找到任何<header>元素的使用,目标是在<header>元素之后立即(或相邻)的<p>元素,并将font-size更改为1.5rem,将font-weight更改为bold。图 1.18 显示了应用于我们的文章的代码。

列表 1.9 相邻兄弟组合符

header + p {
  font-size: 1.25rem;
  font-weight: bold;
}

图 1.18 样式化标题之后的段落

这种方法在尝试将第一个元素与其他元素区分开来,使其突出时可能很有用。我们可能在报纸上看到这种效果。文章的第一段可能会被设计得比其他段落更突出,以吸引我们的注意。

另一个用例是在表单中进行错误处理。相邻兄弟组合符允许我们在表单控件中的无效值之后立即向用户显示错误消息。

通用兄弟组合符(~)

通用兄弟组合符比其他方法更开放,因为它允许我们针对选择器定位的元素之后的所有兄弟元素。

在我们的例子中,我们将为<header>元素之后的所有图像进行样式设计。请注意,我们有三个占位图像。第一个图像很小(它可能是一个标志或作者照片),位于<header>之上。我们不想对它进行样式设计。其他两个图像在文章中更低的位置。我们希望围绕它们添加边框,以保持与文章其余部分的色彩主题一致。

我们的规则将如下所示:header ~ img { border: 4px solid crimson; }。浏览器将找到<header>元素;目标在该元素之后的所有兄弟图像(<img>);并添加一个厚度为4px的边框,这是一条实线(而不是点线、虚线或双线),颜色为crimson。我们可以在图 1.19 中看到应用于我们的文章的代码。

图 1.19 通用兄弟组合符定位标题的兄弟图像

1.6.3 伪类和伪元素选择器

CSS 有称为伪类伪元素的选择器。你可能想知道这些名称的来源。"伪"意味着“不是真正的,虚假的或假装的”。这个定义是有意义的,因为从技术上讲,我们正在定位一个可能还不存在的状态或元素的部分。我们只是在假装。

并非所有伪元素和伪类都适用于所有 HTML 元素。在这本书的整个过程中,我们将探讨我们可以使用伪类的地方以及与哪些 HTML 元素一起使用。

伪类

向选择器添加一个伪类以定位元素的具体状态。伪类对于用户将要与之交互的元素特别有用,例如链接、按钮和表单字段。伪类使用单个冒号(:)后跟元素的状态。

我们的文章包含一些链接。我们没有以任何方式对链接进行样式设计;因此,它们的样式将来自用户代理(UA)样式表。大多数浏览器会在链接下划线并基于链接是否被访问过(即 URL 是否出现在浏览器的历史记录中)以不同的颜色显示它们。

对于链接,我们需要考虑一些状态。最常见的是

  • link—一个锚标签(<a>)包含一个href属性和一个不会出现在用户浏览器历史记录中的 URL。

  • visited—一个锚(<a>)元素包含一个href属性和一个会出现在用户浏览器历史记录中的 URL。

  • hover—用户将光标悬停在元素上但尚未点击它。

  • active—用户正在点击并保持元素。

  • focus—一个聚焦元素是默认接收键盘事件的元素。当用户点击一个可聚焦元素时,它会自动获得焦点(除非某些 JavaScript 改变了这种行为)。使用键盘在表单字段、链接和按钮之间导航也会改变当前聚焦的元素。

  • focus-within—当focus-within应用于父元素,并且父元素的子元素获得焦点时,focus-within样式将被应用。

  • focus-visible—当使用focus-visible选择元素时,样式仅在通过键盘导航获得焦点或用户通过键盘与元素交互时应用。

我们之前提到了:has()。它也是一个伪类,但不仅限于链接,:has()在元素至少有一个符合括号内指定的选择器的后代时应用。当我们编写这本书时,:has()尚未在所有主要浏览器中得到实现。

在我们当前的这篇文章项目中,我们将创建一个 a:link 规则来改变包含 href 属性且未被访问的锚标签的颜色,使其变为浅蓝色,使用十六进制颜色代码 #1D70B8:visited 状态应该与 :link 状态不同,因为它应该向用户表明他们之前未曾访问过该页面(也就是说,URL 不存在于他们的浏览器历史记录中)。通常,网站不会区分这两种状态,但区分它们可以提供更好的用户体验。在我们的例子中,我们将:visited状态改为紫色颜色,使用十六进制代码值 #4C2C92

然后,我们将处理 :hover 状态。此状态不适用于移动用户,因为在移动设备上没有识别用户悬停在链接上的方法。在我们的文章中,我们将:hover状态文本颜色改为深蓝色,使用十六进制代码值 #003078

最后,我们将处理 :focus 状态。我们可以在任何可聚焦元素上使用此状态。链接、按钮和表单字段默认可聚焦(除非禁用),但我们可以通过使用正数 tabindex 使任何元素可聚焦,在这种情况下,可以应用基于聚焦的样式。当用户点击或轻触元素时,将显示 :focus 状态。当元素聚焦时,我们在元素上添加一个 1 像素的深红色轮廓。所有这些加在一起,我们的链接规则如下所示。

列表 1.10 使用伪元素设置链接样式

a:link {
  color: #1D70B8;
}
a:visited {
  color: #4C2C92;
}
a:hover {
  color: #003078;
}
a:focus {
 outline: solid 1px crimson;
}

注意,这些规则集的编写顺序很重要,因为它们具有相同的具体性级别。在样式表中,最底部的条件如果多个条件适用,将获胜。在我们的例子中,如果一个链接已被访问但正在悬停,则该链接将采用由 a:hover {} 规则指定的颜色,因为它在我们的样式表中位于 a:visited {} 规则之后。

虽然开发者工具在功能和访问方式上有所不同,但在大多数浏览器中,我们可以通过进入浏览器,右键单击,并在上下文菜单中选择“检查”来查看不同的元素状态。通常,我们会看到一个带有 CSS 侧边的 HTML 视图。通过点击样式部分中的 :hov 按钮,我们会看到一个可能显示“强制元素状态”的面板,然后我们可以打开和关闭不同的伪类。图 1.20 显示了 Chrome 开发者工具中打开 :hov 面板的情况。

浏览器中的开发者工具

所有主流浏览器都提供了开发者工具,允许开发者修改、调试和优化网站。对于这本书,我们将使用开发者工具来检查我们的代码。我们还将检查浏览器工具中的编译代码,以了解浏览器是如何处理我们的 CSS 的。有关开发者工具及其访问方式的更多信息,请参阅附录。

图片

图 1.20 使用浏览器开发者工具查看不同的元素状态

伪元素

伪元素使用双冒号(::)。伪元素的目的在于允许我们样式化元素的一个特定部分。有时,伪元素会使用单冒号书写,尽管使用两个冒号是强烈推荐的。忽略第二个冒号的能力是为了向后兼容;双冒号语法是作为 CSS3 的一部分引入的,以更好地区分伪类和伪元素。

使用::first-letter伪元素,我们可以定位段落的第一个字母,而不是将其包裹在类似span元素的某物中,这会打断单词并使我们的 HTML 变得杂乱。这种方法允许我们创建复杂的 CSS 而不使 HTML 变得复杂。

在我们的文章中,我们使用了相邻兄弟组合器来使第一段加粗,并且比其他段落的字体大小更大。现在我们将改变第一段第一个字母的颜色,并将其字体样式改为italic

首先,我们定位header元素;然后,我们定位段落(<p>)的第一个字母(::first-letter)。创建选择器后,我们添加我们的声明。我们的 CSS 将如下所示。

列表 1.11 选择第一个字母

header + p::first-letter {
  color: crimson;
  font-style: italic;
}

当此代码应用后,第一个字母是红色的,并且是斜体的(图 1.21)。

图 1.21 伪元素定位在标题后的第一段第一个字母

1.6.4 属性值选择器

属性选择器通常用于样式化链接和表单元素,它样式化包含指定属性的 HTML 元素。属性值选择器寻找具有相同值的特定属性。

在我们的文章中,我们有一些意大利语内容。段落的语言由lang属性指定,如下所示。

列表 1.12 指定意大利语内容

<p lang="it">Questo paragrafo è definito in italiano.</p>

为了提示用户该内容是意大利语,我们将使用 CSS 添加意大利旗标表情。浏览器将找到值为意大利语(it)的lang属性,然后在它之前添加一个意大利旗标表情。列表 1.13 也使用了::before伪元素。我们可以使用多种类型的选择器来定位我们想要样式的 HTML 的精确部分。

列表 1.13 使用多种类型的选择器在意大利语内容前添加旗标

[lang="it"]::before {
  content: "![](https://github.com/OpenDocCN/ibooker-html-css-javascript-zh/raw/master/docs/tn-css-proj/img/icon_Italy.png)"
}

当此代码应用后,我们的意大利语内容前面有一个表情旗标(图 1.22)。

设备和应用中的表情符号差异

如果你正在与本章一起编码,你的输出可能与图 1.22 不同。表情符号在不同的设备、操作系统和应用中呈现方式不同。例如,Emojipedia(emojipedia.org)展示了特定表情符号在应用程序和设备上的外观。你可以在emojipedia.org/flag-italy上找到意大利国旗的详细信息。

图 1.22 通过属性选择器和伪元素应用的意大利国旗

1.6.5 通用选择器

最广泛的选择器类型是通用选择器,它使用星号符号(*)。使用通用选择器做出的任何声明都将应用于所有 HTML 元素。

有时,这个选择器可以用来重置 CSS,但从特定性的角度来看,它具有 0 的特定性值,这意味着如果需要,它可以很容易地被覆盖。这很重要,因为它针对了每个元素。通用选择器还可以用来针对特定选择器的任何和所有后代,例如 .foo * { background: yellow; },其中具有 foo 类的任何和所有后代元素都将被赋予黄色背景。

在我们的示例项目中,我们将使用通用选择器(*)将 font-family 设置为 sans-serif,以便在整个文章中字体始终为无衬线字体,如下面的列表所示。

列表 1.14 使我们的 font-family 保持一致

* { font-family: sans-serif; }

当此代码被应用时,我们文档中的所有文本都将使用无衬线字体,无论元素类型如何(图 1.23)。

图 1.23 使用通用选择器更改所有元素的字体类型

1.7 编写 CSS 的不同方式

CSS 允许我们在编写规则和格式化时具有灵活性。在本节中,我们将探讨缩写属性(我们将在整本书中不断回到这个话题)以及格式化 CSS 的方法。

1.7.1 缩写

缩写 通过将所有值合并到一个属性中来替代编写多个 CSS 属性。我们可以使用一些属性,如 paddingmarginanimation,这些属性在本书的各个部分都有涉及。编写缩写的优点是它减少了样式表的大小,从而提高了可读性、性能和内存使用。

每个缩写属性都有不同的值。让我们探索我们在项目中使用的那个。我们在文章中有一个 blockquote。当我们为其添加样式时,我们使用了 padding 属性,并声明了以下 paddingpadding: 10px。通过这样做,我们使用了缩写。相反,我们本可以像以下列表中所示那样编写代码。

列表 1.15 扩展 padding

  padding-top: 10px;
  padding-right: 10px;
  padding-bottom: 10px;
  padding-left: 10px;

将每个声明单独编写是完全可行的,但从计算性能的角度来看,这样做是昂贵的,尤其是因为所有属性值都是相同的。相反,我们可以使用 padding 属性并将所有四个值放在同一行上。顺序是 toprightbottomleft。如果 right/lefttop/bottom 的值相同,我们还可以将它们组合起来,如图 1.24 所示。

图 1.24 解释缩写属性 padding

如图中所示,我们可以声明所有四个值来定义toprightbottomleft的值。但如果我们说rightleft相同,而topbottom不同,我们可以指定三个值,顺序为topright & leftbottom

如果声明了两个值,我们表示第一个值是topbottom应该是什么;然后第二个值设置rightleft。最后,如果只声明了一个值,该值将设置所有四个边。

1.7.2 格式化

我们可以用几种方式编写 CSS,并且当我们查看他人的代码时,我们经常看到不同的格式。本节展示了几个示例。

列表 1.16 中所示的多行格式可能是格式化的最流行选择。每个声明都在自己的行上,并通过制表符或空格缩进。

列表 1.16 多行格式

h1 {
  color: red;
  font-size: 16px;
  font-family: sans-serif
}

列表 1.17 所示的多行格式的变体将开括号放在自己的行上。这个例子可能是我们在 PHP 语言中看到的东西。将开括号放在自己的行上可能被认为是多余的。

列表 1.17 多行格式的变体

h1
{
  color: red;
  font-size: 16px;
  font-family: sans-serif
}

列表 1.18 中所示的单行格式非常有意义;它紧凑,我们可以扫描一个文件,知道第一部分是选择器。缺点是如果规则包含许多声明,它可能难以阅读。

列表 1.18 单行格式

h1 { color: red; font-size: 16px; font-family: sans-serif }

所有这些选项都有优点和缺点,但本书中的项目使用了一和三的组合。要知道的主要是,没有正确或错误的方法;选择通常取决于对你和/或你的团队来说什么最有效。只要代码易于理解,那就足够了。

那些有鹰眼的人会注意到,在列表 1.16、1.17 和 1.18 中,规则的最后一个声明的末尾没有分号(;)。这个分号是可选的。CSS 最好的一个方面之一是我们可以用最舒适的方式编写它。

摘要

  • CSS 是一种成熟的编码语言,CSS 的每一部分都由模块组成。

  • 模块替换了像 CSS3 这样的大型发布。

  • 内联 CSS 可以具有最高优先级,并且性能良好,但它重复且难以维护。

  • 外部 CSS 将我们的 CSS 与 HTML 分开,保持 SoC。

  • 除了我们自己的 CSS 外,浏览器还会应用默认样式。

  • 用户还可以应用他们自己的 CSS,这可以覆盖作者和 UA 样式表。

  • 使用!important被认为是不良实践。

  • 一个 CSS 规则由一个选择器和一条或多条声明组成。

  • 我们可以为许多类型的选择器创建规则,并且每个规则都可以有自己的特定级别。

2 使用 CSS Grid 设计布局

本章涵盖

  • 探索网格轨道并排列我们的网格

  • 在 CSS 网格中使用minmaxrepeat函数

  • 使用 CSS Grid 独特的分数单位

  • 创建模板区域并将项目放置在区域中

  • 使用网格时考虑可访问性

  • 在网格中创建列和行之间的间隙

现在我们对 CSS 的工作原理有了基本的了解,我们可以开始探索布局 HTML 内容的选项。在本章中,我们将专注于使用网格进行布局。

2.1 CSS Grid

在这种意义上,网格是由交叉形成一系列正方形或矩形的线条网络。现在所有主流浏览器都支持 CSS Grid,它已成为一种流行的布局技术。

实际上,网格由列和行组成。我们将创建我们的网格,然后为我们的项目分配位置,就像我们在玩棋盘游戏“战舰”时在网格上放置船只一样。

虽然网格布局有时与表格相比较,但它们有不同的用途和满足不同的需求。网格用于布局,而表格用于表格数据。如果正在设计的样式内容适合 Microsoft Excel 表格,那么它是表格数据,应该放在表格中。

在 2000 年代中期,我们使用表格进行布局,有时我们仍然需要这样做。(例如,电子邮件有时需要使用表格进行布局,因为它们只支持 CSS 样式的一个子集。)然而,在网络上,这种技术被认为是不良实践,因为它会导致可访问性差和语义缺失。现在我们可以使用网格代替。

CSS Grid 赋予我们创造力,可以制作各种布局,并配合媒体查询适应不同条件。我们将使用网格来设计我们的项目,到本章结束时,我们的布局将看起来像图 2.1。

图 2.1 最终输出

我们在 GitHub 仓库的 chapter-02 文件夹中(github.com/michaelgearon/Tiny-CSS-Projects)或 CodePen(codepen.io/michaelgearon/pen/eYRKXqv)的起始 HTML,看起来如下所示。

列表 2.1 项目 HTML

<body>
  <main>                                   ①
    <header>                               ②
      <img src="./img/logo.jpg" alt="">
      <h1>Sonnets by William Shakespeare</h1>
    </header>
    <article>                              ②
      <h2>
        Sonnet 1
        <br><small>by William Shakespeare</small>
      </h2>
      <p>
        <span>From fairest creatures we desire increase,</span>
        ...
      </p>  
    </article>
    <aside>                                ②
      <section>
        <img src="./img/image-1.jpg" alt="">
        <h3>Sonnet 2</h3>
        <p>
          When forty winters shall besiege thy brow,
          <br>And dig deep trenches in thy beauty's field, ...
        </p>
        <a href="">Read more of Sonnet 2</a>
      </section>
      <section>
        <img src="./img/image-2.jpg" alt="">
        <h3>Sonnet 3</h3>
        <p>
          Look in thy glass and tell the face thou viewest,
          <br>Now is the time that face should form another, ...
        </p>
        <a href="">Read more of Sonnet 3</a>
      </section>
    </aside>
    <section class="author-details">       ②
      <h3>
        <small>About the Author</small>
        <br>William Shakespeare
      </h3>
      <p>English playwright, poet, ...</p>
    </section>
    <section class="plays">                ②
      <img src="./img/play.jpg" alt="">
      <h3>Checkout out his plays:</h3>
      <ul>
        <li><a href="">All's Well That Ends Well</a></li>
        ...
      </ul>
    </section>
    <footer>                               ②
      <p>Want to read more ...</p>
    </footer>
  </main>
</body>

① 我们项目的容器

② 我们容器内的子元素

我们还有一些起始 CSS(列表 2.2),以指导我们在开始将 HTML 元素放置在网格格式时。在本章中,我们不会担心这些预设样式(如边距、填充、颜色、字体和边框)。这些概念在其他部分的书中有详细说明,因为我们想专注于本项目的布局。

列表 2.2 起始 CSS

body {
  margin: 0;
  padding: 0;
  background: #fff9e8;
  min-height: 100vh;                         ①
  font-family: sans-serif;
  color: #151412
}
main { margin: 24px }
img {
  float: left;                               ②
  margin: 12px 12px 12px 0
}
main > * {                                   ③
  border: solid 1px #bfbfbf;                 ④
  padding: 12px;
}
main > *, section { display: flow-root }     ⑤
p, ul { line-height: 1.5 }
article p span { display: block; }
article p span:last-of-type,                 ⑥
article p span:nth-last-child(2) {           ⑥
  text-indent: 16px                          ⑥
}                                            ⑥
.plays ul { margin-left: 162px }             ⑦

① 背景覆盖整个页面,即使窗口比内容长。

② 允许文本围绕图像换行

③ 星号和子组合选择主元素的所有直接子元素。

④ 边框指出通过网格定位的章节。

⑤ 防止图像从其容器中溢出

⑥ 缩进十四行诗的最后两行

⑦ 缩进列表;否则,项目符号会紧挨着图像。

我们将字体从serif改为sans-serif,并使用margin增加浏览器窗口边界和容器之间的间距。我们还使图像左浮动,并调整行高、字体和填充。

注意,我们在main元素的直接子项上添加了边框和一些填充,以帮助我们定义布局。我们将在项目的后期移除这些元素。我们的起始点看起来像图 2.2。

图 2.2 起始点

CSS 网格是一种在 2D 布局上放置项的方法:水平(x 轴)和垂直(y 轴)。相比之下,flexbox(在第六章中介绍)是单轴导向的。它仅在其配置的 x 轴或 y 轴上操作。

我们可以使用 CSS Flexbox 和 CSS Grid 在网页上对齐和布局项。但随着我们进入本章,我们会发现 Grid 相对于 Flexbox 的一个好处是,它允许我们轻松地将页面划分为区域并创建复杂的布局。

首先,我们将设置我们的网格。然后我们将探讨根据窗口大小如何改变网格的行为。

2.2 显示网格

安排网格的第一步是将父容器项的display值设置为grid。在创建网格布局时,我们可以使用两个值之一:

  • grid—当我们希望浏览器在块级框中显示网格时使用。网格占据容器的全部宽度,并自行在新行上设置。

  • inline-grid—当我们希望网格成为内联级框时使用。网格将自己设置为与之前内容的内联,就像一个<span>

我们将使用列表 2.3 中显示的grid值进行布局。

块级和内联级框的区别

在 HTML 中,每个元素都是一个框。块级框表示一个元素的框应该使用其父元素的整个水平空间,因此默认情况下阻止任何其他元素位于同一水平线上。相比之下,内联元素允许其他内联元素位于同一水平线上,具体取决于剩余空间。

列表 2.3 设置显示为grid

main {
  display: grid;
}

如果我们在浏览器中预览此代码,我们会注意到视觉上没有任何变化,因为浏览器默认情况下将直接子项显示在一列中。然后浏览器为所有子元素生成足够的行。

使用我们浏览器中的开发者工具(图 2.3),我们看到,尽管布局在视觉上没有改变,但网格已经被创建。要在大多数浏览器中查看底层网格,我们可以右键点击网页,并在上下文菜单中选择“检查”。在 Mozilla Firefox 的检查窗口中,当我们选择父容器时,我们看到两个指示布局现在是网格的东西:

  • 每个直接子项周围都有紫色线条。

  • 在 HTML 中,<main>元素中有一个名为 grid 的图标。当我们点击<main>旁边的网格图标时,布局面板会显示我们的网格结构。

图 2.3 Firefox 的开发工具

我们可以在 Google Chrome 或 Apple 的 Safari 中遵循类似的步骤。

2.3 网格轨道和线

当 CSS 网格布局模块被引入时,它引入了新的术语来描述布局项。这些术语中的第一个是网格线。网格线水平垂直排列,并从左上角开始编号为 1。在正数对面的是负数。

书写模式和脚本方向

每一行分配的数字取决于书写模式(文本行是水平还是垂直排列)以及组件的脚本方向。例如,如果书写模式是英语,那么左边的第一行编号为 1。如果由于语言而将语言方向设置为从右到左,例如阿拉伯语(从右到左书写),则第 1 行将是最右侧的行。

网格线之间的空间被称为网格轨道,它们由列和行组成。列从左到右排列,行从上到下排列。轨道是网格上任意两条线之间的空间。在图 2.4 中,高亮的轨道是我们网格中的第一行轨道。列轨道将是两条垂直线之间的空间。

图 2.4 基于英语书写模式,方向设置为从左到右的网格结构

每个轨道中都有网格单元格。单元格是网格行和网格列的交点。

我们可以使用grid-template-columnsgrid-template-rows属性来布局我们的网格。这些属性指定了一个以空格分隔的轨道列表,包括网格的线名和轨道尺寸函数。grid-template-columns属性指定了网格列的轨道列表,而grid-template-rows指定了网格行的轨道列表。

在我们设置列之前,我们需要了解一些 CSS 网格特有的概念。

2.3.1 重复列

为了在代码中避免重复,您可以使用repeat()函数来指定您需要的列数或行数。

定义:函数是一个自包含的、可重用的代码片段,具有特定的角色。函数存在于其他编程语言中,如 JavaScript。有时,我们可以向函数传递一个或多个值;这些值被称为参数。向函数传递值时,我们将它们放在括号中。我们无法在 CSS 中创建自己的函数;相反,我们使用 CSS 提供的内置函数。

repeat()函数需要两个以逗号分隔的值。第一个值表示要创建多少列或行。第二个值是每列或每行的尺寸。

对于我们的项目,我们将指定我们想要两列,并且对于每列的大小,我们将使用 minmax() 函数。因此,我们的列定义将是 grid-template-columns: repeat(2, minmax(auto, 1fr)) 250px;。如果我们正在定义行的高度,我们将使用 repeat()grid-template-rows

这个声明生成了三列,其中两列使用分数单位具有相同宽度,一列为 250 像素。让我们进一步看看这个声明。注意在 repeat() 函数内部,我们使用了 minmax() 函数。

2.3.2 minmax() 函数

minmax(min, max) 函数由两个参数组成:网格轨道的最小和最大范围。万维网联盟(W3C)规范指出,minmax 函数“定义了一个大于或等于 min 且小于或等于 max 的大小范围”(www.w3.org/TR/css-grid-2)。

注意:为了使函数有效,min 值(第一个参数)需要小于 max 值。否则,浏览器会忽略 max,函数仅依赖于 min 值。

对于我们的项目,我们将 min 值设置为 auto,将 max 值设置为 2。让我们看看 auto 的含义。

2.3.3 自动关键字

auto 关键字可以在函数的最低或最高值中使用。当 auto 关键字用于最大值时,它被处理成与 max-content 关键字相同。行或列的尺寸将与行或列内内容所需的空间相等。

虽然我们没有在我们的项目中使用它,但 auto 关键字的一个常见用例是创建包含固定页眉和页脚的布局。当我们为设置为 auto 的区域分配溢出时,该区域会随着窗口大小而收缩和扩展,如图 2.5 所示。

图片 2-5

图 2.5 使用 auto 关键字的示例

对于我们的用例,在声明 grid-template-columns: repeat(2, minmax(auto, 1fr)) 250px; 中,auto 关键字规定,对于我们的前两列,列的最小宽度应与包含的元素宽度相同。让我们看看用来设置最大宽度的灵活长度单位(fr)。

2.3.4 分数(fr)单位

分数单位(fr)是在 CSS 网格布局模块中引入的。fr 单位是网格特有的,它告诉浏览器一个 HTML 元素应该有多少空间,与其他元素相比,这是通过在应用最小值之后分配剩余空间来实现的。CSS 将可用空间平均分配给 fr 单位,因此 1fr 的值等于可用空间除以指定的 fr 单位总数。

让我们通过图 2.6 中显示的美味蛋糕图解来探索分数的含义。(如果这个图让你想吃一块蛋糕,那很抱歉。)

图片 2-6

图 2.6 分数值

如果你有一个完整的蛋糕,它等于 100%。从 CSS 的角度来看,如果我们决定吃掉所有的蛋糕,那将是 1 个分数。在我们的 CSS 中,这将是grid-template-columns: 1fr,这将等于列的 100%。

但我们是友好的,所以我们决定把一些蛋糕分给四个朋友。我们需要确定每个人将有多少份蛋糕。

如果我们公平的话,可以说我们的蛋糕可以分成四等份。在我们的 CSS 中,这将是grid-template-columns: 1fr 1fr 1fr 1fr。我们告诉浏览器给每个 HTML 元素一个相等的整体份额。

但如果我们决定偷偷地多留一些给自己呢?毕竟,是我们烤的蛋糕。我们决定拿走一半的蛋糕,然后把剩下的另一半分成三份。为了做到这一点,我们需要六个分数:三个分数代表我们 50%的蛋糕,然后对剩下的 50%再分三次。

我们的 CSS 将是grid-template-columns: 3fr 1fr 1fr 1fr。所以我们在说总共有六个分数;第一列得到三个(或者说总量的 50%),然后剩下的 50%应该平均分配给其他三个列。我们可以使用fr单位和repeat()函数来使这个值更容易阅读,这将grid-template-columns: 3fr repeat(3, 1fr)。

对于我们的项目,我们将通过在以下列表中添加代码到main规则中来创建列的网格线。

列表 2.4 设置列的数量

main {
  display: grid;
  grid-template-columns: repeat(2, minmax(auto, 1fr)) 250px;
}

在浏览器中预览时(图 2.7),我们看到现在我们的网格在每个线上都设置了数字。我们可以利用这些信息,明确选择在网格中放置我们的 HTML 元素的位置,基于网格线的编号。

图片

图 2.7 Firefox 浏览器预览显示的网格线和每行的相关数字

我们还注意到,浏览器假设我们想要在每个网格单元内放置我们的 HTML 元素。而不是垂直堆叠元素,浏览器会填充每个列单元,直到填满,然后创建一个新的行,并填充那些列。自动创建额外的网格单元也被称为隐式网格

显式网格与隐式网格

当我们使用grid-template-columnsgrid-template-rows时,我们正在创建一个显式网格。我们明确地告诉浏览器这个网格应该有多少列和行。

隐式部分(对于行和列)是浏览器自动创建的部分,这发生在子项多于网格单元的情况下。在这种情况下,浏览器隐式地向我们的网格添加单元格,以确保所有元素都有一个网格单元。

我们可以通过grid-auto-flowgrid-auto-columnsgrid-auto-rows来控制隐式行为。

在这个阶段,我们已经创建了一个包含三列的网格。其中两列使用 minmax(),我们的第三列有一个固定的值 250px。这些设置给我们一个三列布局。我们希望将主要内容分布在第一列和第二列中,并使用第三列来显示不太重要的内容,这就是为什么我们给它较少的视觉空间。(在大多数屏幕上,第三列将比前两列窄。)

2.4 网格模板区域

如果我们想在网格的特定行和列上显式设置一个元素,我们有两种选择。首先,我们可以使用行号并指定子元素的如下位置:grid-column: 1 / 4。在这个语法中,第一个数字表示元素开始的位置,第二个数字表示元素结束的位置(图 2.8)。这个例子将元素放置在第一列,跨越第二列和第三列。如果只提供一个数字,则元素只跨越一行或一列。

图片 2-08

图 2.8 示例 grid-columngrid-row 语法

要定义行,我们会使用与列相同的语法,通过 grid-row 属性。为了放置一个元素,使其从第三行开始并跨越两行,我们会写 grid-row: 3 / 5grid-columngrid-row 属性是 grid-column-startgrid-column-endgrid-row-startgrid-row-end 的缩写。

我们可以不用处理数字,而使用命名区域来引用,当我们明确地将元素放置在网格上时。为此,我们使用 grid-template-areas 属性,它允许我们定义我们希望网页如何布局。

grid-template-areas 属性接受多个字符串,每个字符串由它们描述的区域的名称组成。每个字符串代表布局中的一行,如图 2.8 所示。每个名称代表行内的一个列。如果两个相邻的单元格具有相同的名称(水平或垂直),则这两个单元格被视为一个区域。网格区域可以是一个单独的单元格,例如图 2.9 中定义的 plays 区域,但如果它包含多个单元格,则这些单元格必须创建一个矩形形状,并且所有具有相同名称的单元格必须是相邻的。例如,您无法创建一个 L 形状。

图片 2-09

图 2.9 grid-template-areas 属性的语法

命名区域的优点在于最终结果的可视化。我们将定义我们的 grid-template-areas,如列表 2.5 所示。注意图 2.9 中第四行的点(.)。点用于定义我们打算保持为空的单元格。因为那个单元格没有名称,所以内容不能分配给它。

列表 2.5 创建我们的模板区域

main {
  display: grid;
  grid-template-columns: repeat(2, minmax(auto, 1fr)) 250px;
  grid-template-areas:
  "header  header  header"
  "content content author"
  "content content aside "
  "plays      .    aside "
  "footer  footer  footer";
}

尽管我们已经定义了区域,但内容仍然隐式地定位在每个可用的单元格中,忽略了我们定义的区域(图 2.10)。我们需要将这些内容分配给这些区域的每一个。

图片 2-10

图 2.10 Firefox 中显示的已定义网格区域

2.4.1 grid-area 属性

为了将元素放置在定义的区域中,我们使用 grid-area 属性。它的值是在 grid-template-areas 属性中分配的名称。如果我们想将 <header> 元素放置在我们定义为 header 的区域内部,例如,我们将定义 header { grid-area: header; }。对于我们的项目,我们按照以下列表所示在我们的网格上设置元素。

列表 2.6 将内容分配给网格区域

header { grid-area: header }
article { grid-area: content }
aside { grid-area: aside }
.author-details { grid-area: author }
.plays { grid-area: plays }
footer { grid-area: footer }

现在我们已经明确定义了内容应该放置的位置,内容就会落到位(图 2.11)。

图片

图 2.11 明确放置在网格上的内容

在布局设置中,让我们移除一些为了理解我们的布局做了什么而添加的样式。如下所示列表中,我们移除了内容部分的填充和边框。

列表 2.7 移除调试样式

main > * {
 border: solid 1px #bfbfbf;
 padding: 12px;
}

移除这些样式并缩小屏幕宽度(图 2.12)后,相邻列或行中的内容看起来更靠近。

图片

图 2.12 窄屏幕宽度

让我们在区域之间添加空间。为了完成这个任务,我们将使用 gap 属性。

2.4.2 gap 属性

gap 属性是 row-gapcolumn-gap 属性的简写。通过设置行和列的间隙,我们定义了行和列之间的空白。空白是来自印刷设计的术语,定义了列之间的间隙。默认情况下,列和行之间的间隙是关键字 normal。这个值在所有上下文中都等于 0px,除非它与 CSS 多列模块一起使用,这时它等于 1em

当我们使用 gap 属性时,额外的空间仅应用于网格的轨道之间。在第一个轨道之前或最后一个轨道之后不应用空白。为了设置网格周围的空间,我们使用填充和边距属性。

gap 与 grid-gap 的比较

当 CSS Grid 正在被定义时,这个属性的规范被称为 grid-gap 属性,但现在推荐使用 gap。我们可能在旧项目中看到 grid-gap

gap 属性可以有最多两个正值。第一个值设置 row-gap,第二个值用于 column-gap。如果只声明了一个值,它将应用于 row-gapcolumn-gap 属性。

对于我们的项目,我们将通过在 main 规则中添加 gap: 20px 来设置行和列之间的 20px 间隙。图 2.13 显示了添加到布局中的间隙。添加了间隙后,让我们将注意力转向根据屏幕大小调整布局。

图片

图 2.13 添加了间隙的网格布局

2.5 媒体查询

CSS 允许我们根据条件应用样式。一种条件是屏幕大小。媒体查询是规则:它们以@符号开头,并定义了包含它们的样式应在何种条件下应用。如果我们查看宽屏上的当前布局(图 2.14),我们会注意到页面中心有大量空间可以更好地利用。

图片

图 2.14 宽屏上的布局

让我们创建一个针对宽度大于955px的屏幕的媒体查询。查询是@media (min-width: 955px) { }。我们放在大括号{}内的所有规则仅在屏幕大小大于或等于955px时应用。

列表 2.8 显示了我们的媒体查询。如果满足媒体查询条件,我们重新定义grid-template-areas以具有不同的配置。我们还更新了列的大小,以便列具有相等的宽度。

列表 2.8 使用媒体查询创建我们的模板区域

@media (min-width: 955px) {                    ①
  main {
    grid-template-columns: repeat(3, 1fr);     ②
    grid-template-areas:                       ③
    "header  header  header"                   ③
    "content author aside"                     ③
    "content plays aside "                     ③
    "footer  footer  footer";                  ③
  }
}

① 与媒体特性一起的 at 规则

② 重新定义列的大小

③ 重新配置内容应放置的位置

现在布局看起来像图 2.15 和图 2.16。

图片

图 2.15 窄屏使用原始布局。

grid-template-areas与媒体查询结合使用,我们可以用最少的代码重新配置我们的布局。但我们必须避免一些可访问性的陷阱。

图片

图 2.16 宽屏使用媒体查询的布局。

2.6 可访问性考虑

当我们将项目放置在网格区域内时,我们主要保持了它们在 HTML 中出现的顺序:页眉保持在顶部,页脚保持在底部,内容以逻辑的视觉顺序排列。但如果 HTML 顺序和视觉显示顺序不同怎么办?

如果用户正在使用屏幕阅读器跟随或通过键盘导航页面,并且程序性顺序与显示的内容不匹配,那么行为看起来将是随机的。这种随机性将使用户难以导航页面并理解其内容。通过使用网格改变内容的位置的视觉变化不会影响辅助技术向用户呈现信息的顺序。W3 网格布局模块建议如下关于这种情况(mng.bz/xdD7):

作者必须仅使用顺序和网格放置属性进行视觉上的内容重新排序,而不是逻辑上的重新排序。使用这些功能进行逻辑重新排序的样式表是不符合规范的。

解决方案是保持源代码和视觉体验相同,或者至少在合理的顺序上。这种方法既提供了最可访问的 Web 文档,又提供了一个良好的工作结构。对于英语来说,这意味着内容和 HTML 应该遵循相同的顺序,从左上角到右下角。

在将我们的元素分配到网格的相应区域后,我们应该始终测试我们的页面,以确保无论用户如何访问页面,顺序都是合理的。一种方法是使用屏幕阅读器访问我们的页面,并通过标签顺序来确保标签顺序仍然有效。

一些工具和扩展可以帮助可视化标签顺序。例如,在 Firefox DevTools 中,我们可以选择“无障碍性”选项卡并勾选“显示标签顺序”复选框,如图 2.17 所示,它会突出显示并编号可聚焦元素。我们可以看到我们的标签顺序是合理的,不太可能让用户感到困惑,所以我们可以继续前进。

图 2.17 Firefox DevTools 中暴露的 HTML 标签顺序

现在我们的项目已经完成(图 2.18)。

网格的未来

在本章中,我们使用了 CSS 网格布局模块来创建一个根据浏览器宽度做出响应的布局。网格的许多方面仍在开发和迭代中,最值得注意的是子网格,这将允许网格中有网格。

虽然你现在可以在网格内设置网格,但子网格的好处是它们与其父网格更紧密相关。为了关注未来的增强和发展,请查看网格规范www.w3.org/TR/css-grid

图 2.18 宽屏上的最终产品

摘要

  • 网格是由交叉形成一系列正方形或矩形的线条网络。

  • 值为 griddisplay 属性允许我们在网格布局上放置项目。

  • display 属性应用于包含要放置在网格上的子元素的父项。

  • grid-template-columnsgrid-template-rows 属性用于显式定义网格应包含的列和行的数量和大小。

  • 可伸缩长度 (fr) 单位是 CSS 网格作为设置项目维度的一种替代方式而形成的测量单位。

  • 我们可以使用 repeat() 函数提高代码效率,其中一行或多列具有相同的大小。

  • minmax() 函数允许我们设置两个参数:列应具有的最小宽度和最大宽度。

  • grid-template-areas 属性允许我们定义每个网格区域被称为什么。然后我们可以使用子项上的 grid-area 属性将它们分配到那些命名位置。

  • gap 属性在网格单元之间添加间距(创建边距)。

  • 源代码和视觉体验需要保持相同的逻辑顺序。如有疑问,我们可以使用浏览器开发者工具来检查标签顺序。

3 创建一个响应式的动画加载界面

本章涵盖

  • 使用可缩放矢量图形 (SVG) 创建基本形状

  • 了解 SVG 中 viewbox 和 viewport 的区别

  • 理解关键帧和动画 SVG

  • 使用动画属性

  • 使用 CSS 样式化 SVG

  • 使用外观属性样式化 HTML 进度条元素

我们在当今的大多数应用程序中都能看到加载器。这些加载器向用户传达正在加载、上传或等待的信息。它们给用户带来信心,表明正在发生某些事情。

如果没有某种指示器告诉用户正在发生某些事情,他们可能会尝试重新加载,再次点击链接,或者放弃并离开。当操作超过 1 秒时,我们应该使用某种进度指示器,这时用户往往会失去注意力并质疑是否存在问题。除了显示正在发生的事情的图形外,加载器还应伴随文本,告诉用户正在发生什么,以改善屏幕阅读器和其他辅助技术的网页可访问性。

对于我们的动画,我们将研究 CSS 动画模块,理解动画属性、关键帧和过渡,以及可访问性和对用户偏好的尊重。

3.1 设置

在这个项目中,我们将在 SVG 中创建矩形。我们将了解 SVG 提供的内容,并理解样式化 HTML 元素和 SVG 元素之间的细微差别。

我们还将创建一个进度条,显示用户完成了多少任务以及还剩下多少任务。我们将使用 HTML <progress> 元素,然后查看我们如何编辑浏览器的默认样式并应用自己的样式。总的来说,我们希望创建一个一致、响应式的加载器,能够在各种设备上工作。图 3.1 显示了结果。

图 3.1 本章目标

本项目的代码位于 GitHub 仓库中(github.com/michaelgearon/Tiny-CSS-Projects)的第三章文件夹内。您可以在 CodePen 上找到完成项目的演示 codepen.io/michaelgearon/pen/eYvVVre

3.2 SVG 基础

SVG 代表 可缩放矢量图形。SVG 是一种基于 XML 的标记语言,由笛卡尔平面上的矢量组成。矢量图形可以从头开始编写,但通常是在 Adobe Illustrator、Figma 或 Sketch 等图形程序中创建。然后以 SVG 文件格式导出,并可以在代码文本编辑器中打开。

向量 是一个定义了几何原初的数学公式。线条、多边形、曲线、圆和矩形都是几何原初的例子。

平面上的 笛卡尔坐标系 是一个基于网格的系统,它使用一对基于点到两条垂直轴距离的数值坐标来定义一个点。这两条轴交叉的位置是 原点,其坐标值为 (0, 0)。回想一下数学课;当被要求在图上绘制线条时,你就是在使用笛卡尔坐标系。本质上,SVG 是用 XML 编写的坐标平面上的形状。

相比之下,PNG、JPEG 和 GIF 是 位图图像,它们是通过使用像素网格创建的。图 3.2 展示了位图和矢量图形之间的区别。

图片

图 3.2 矢量图形与位图图形的比较

SVG 相比于位图图像有许多优势,包括无限可缩放。我们可以将图像缩小或放大到我们想要的程度,而不会丢失质量。我们无法在不看到 像素化 的情况下放大位图图像,这是由于放大渲染网格中的像素网格,使得网格的各个小方块变得可见。相比之下,当我们放大 SVG 时,我们是在坐标平面上程序化地设置形状和线条;点之间的路径被重新绘制,质量不会降低。

因为 SVG 是用 XML 编写的,所以我们可以直接在我们的 HTML 中放置 SVG 代码,并以与我们处理其他 HTML 元素相同的方式访问、操作和编辑它。SVG 对于图形来说,就像 HTML 对于网页一样。

然而,对于处理高度复杂的图像,如照片,位图是一个更好的选择。使用 SVG 创建逼真的图像是可能的,但这并不实用。矢量图形的文件大小和因此的加载性能比位图图像大得多。

SVG 最常见的用途是标志、图标和加载器。我们使用它们作为标志,因为标志通常是简单的图像,需要无论大小或媒介都保持清晰。此外,一个公司或产品拥有几个版本的标志,用于深色背景和浅色背景的情况并不少见。重新着色、简洁和缩放也是我们为什么使用 SVG 作为图标的原因。

我们使用 SVG 作为加载器,因为与它们的位图对应物不同,它们允许我们在图像内部添加动画。我们可以将图形内的单个元素隔离开来,并应用 CSS 或 JavaScript 到这个单独的部分——这是位图无法做到的。

之前我们提到 SVG 是基于笛卡尔坐标系(一个二维坐标平面)。让我们来看看这意味着什么以及它是如何工作的。

3.2.1 SVG 元素的定位

当我们处理 SVG 元素时,考虑定位的方式是想象我们在一个网格上放置元素。一切从(0,0)(原点)开始,这是 SVG 文档的左上角。xy值越高,它离左上角就越远。图 3.3 在图 3.2 的船的例子上进行了扩展,增加了每个形状的原点和坐标值。

图片

图 3.3 在坐标平面上定位元素

我们项目中的加载器由一系列 11 个矩形组成。为了放置它们,我们需要考虑它们在坐标平面上的位置,包括它们的宽度和它们之间的间隙。

3.2.2 视口

视口是用户可以看到 SVG 的区域。它由两个属性设置:widthheight。将视口想象成一个画框:它设置了画框的大小,但不会影响其中图形的大小。如果我们在一个比画框大的画框内放置一个图像,那么就会发生溢出。同样的事情也会发生在我们的 SVG 上。在 CSS 定位中,视口测量值以 SVG 的左上角为起点(图 3.4)。

图片

图 3.4 带有和没有定义视口的 SVG

加载器的视口将是

<svg width="100%" height="300px"> <!--SVG code --> </svg>

宽度设置为100%,但 100%是指什么?我们规定加载器将占用其父项提供的 100%的可用空间。

以下列表显示了我们的起始 HTML。我们看到我们的加载器嵌套在一个部分中;因此,我们的加载器将与该部分的宽度相同。

列表 3.1 开始使用 HTML

<body>
  <section>
    <svg width="100%" height="300px"></svg>          ①
    <h1>Scanning channels</h1>
    <p>This may take a few minutes</p>
    <progress value="32" max="100">32%</progress>    ②
  </section>
</body>

① 加载器增加了 100%宽度、300 像素高度的视口

② 我们将在本章后面讨论的进度条

我们还有一些起始 CSS(列表 3.2)。背景(<body>)、<section>、标题(<h1>)和段落(<p>)已经被预先样式化,以关注加载器、进度条和动画。

列表 3.2 开始使用 CSS

body { background: rgb(0 28 47); }
section {                                ①
  display: flex;                         ②
  flex-direction: column;                ②
  justify-content: space-between;        ②
  align-items: center;                   ②
  max-width: 800px;                      ②
  margin: 40px auto;                     ③
  font: 300 100% 'Roboto', sans-serif;   ④
  text-align: center;                    ④
  color: rgb(255 255 255);               ⑤
}                                        ⑥
h1 {
  font-size: 4.5vw;
  margin: 40px 0 12px;
}

p {
  font-size: 2.8vw;
  margin-top: 0;
}

① 规则样式开始,用于样式化加载器的容器

② 使用 flexbox 布局,将子项设置为列方向,水平居中,并设置元素之间的等间距

③ 使用缩写属性编写的边距:顶部和底部,40 像素边距;左侧和右侧,自动

④ 字体设置将字体重量设置为轻,使用 Roboto 字体,回退为无衬线字体,并居中文本

⑤ 使用 RGB 设置颜色为白色

⑥ 规则样式结束,用于样式化加载器的容器

我们看到我们的部分宽度被限制在 800 像素。<section>是一个块级元素,因此默认情况下,它将占用其可用的全部宽度。《body》和<html>也是块级元素。

由于我们没有在<body><html>上指定宽度、填充或边距,它们将占据整个窗口宽度。<section>将占据<body>的整个宽度。但由于我们为<section>分配了最大宽度,当窗口宽度达到 800 像素时,部分将停止与<body>一起增长,并保持 800 像素宽。由于部分元素有顶部和底部边距为40px,它将略微增加浏览器窗口和元素之间的间隙。

我们的加载器包含在部分中。该部分将占据整个身体的宽度,直到达到 800 像素;因此,我们的加载器也将这样做。图 3.5 显示了加载器的宽度将如何受到屏幕尺寸的影响。

图片

图 3.5 使用max-width时 SVG 宽度受窗口宽度的影响

设置了视口后,让我们设置视图框,以便 SVG 的内容可以与其容器一起缩放。记住,到目前为止,我们只处理了框架,而不是其内部。

3.2.3 视图框

视图框设置了图形在视口内的位置、高度和宽度。之前,我们将视口比作画框。视图框允许我们调整图像以适应我们的画框。它可以定位图像,也可以缩放图形,使其适合画框。我们可以将视图框视为我们的平移和缩放工具。要设置视图框,我们将viewBox属性应用于 SVG,并使用以下四个值和语法:viewBox="min-x min-y width height"。列表 3.3 显示了viewBox应用于我们的加载器。

按顺序分析数字,我们从min-xmin-y开始,它们都设置为0。我们希望图形的左上角位于框架的左上角。min-xmin-y允许我们调整图形在其框架中的位置;它是平移工具。由于我们希望它正好位于左上角,我们将值设置为0

接下来,我们应用宽度,设置为710,因为我们的加载器有 11 个总条形,每个条形宽度为60。60 × 11 = 660,我们有 10 个间隙。每个条形之间的间隙宽度为 5 × 10 = 50;因此,我们的加载器宽度将是660 + 50 = 710

我们将基于加载器中条形的高度设置viewBox的高度。条形的高度值为300,因此我们也设置了视口高度为300。我们的加载器将正好适合其视口。下一个列表显示了应用于 SVG 的viewBox

列表 3.3 声明视图框

<svg viewBox="0 0 710 300" width="100%" height="300px">
 <!--SVG code-->
</svg>

注意,我们的视图框和视口高度都等于300。这就是我们缩放的方式。如果视图框的数字小于视口的数字,我们实际上是在框架外缩放,图形将更小。如果视图框的数字大于视口的数字,我们是在缩放。然而,由于我们的视口和视图框高度相等,所以我们没有缩放。

现在我们已经定义了我们将在其中工作的空间,我们可以开始向加载器添加形状。

3.2.4 SVG 中的形状

SVG 中有一些标准的形状和元素:

  • rect(矩形)

  • circle

  • ellipse

  • line

  • polyline

  • polygon

如果我们要创建一个不规则形状,我们也可以使用 path,但在这个加载器中我们不需要它。通常,路径是我们查看标志、图标和复杂动画图形背后的 XML 时看到的。对于我们的项目,我们将使用基本的矩形形状来创建波浪。

为了定义我们的矩形,这些矩形将在加载器中创建条形,我们将使用 <rect> 元素并添加四个属性:heightwidthxyxy 属性决定了矩形相对于 SVG 左上角的位置。

我们想要创建 11 个矩形(列表 3.4),宽度为60,高度为300,我们将使用 x 属性在图形中移动矩形。我们从0开始,并将值增加条形的宽度(60)加上额外的间隙5。每个矩形的 x 值将比前一个多65。我们的第 11 个矩形的 x 值应该是650

列表 3.4 十一个矩形

    <svg viewBox="0 0 710 300" width="100%" height="300">
        <rect width="60" height="300" x="0" />
        <rect width="60" height="300" x="65" />
        <rect width="60" height="300" x="130" />
        <rect width="60" height="300" x="195"/>
        <rect width="60" height="300" x="260"/>
        <rect width="60" height="300" x="325"/>
        <rect width="60" height="300" x="390"/>
        <rect width="60" height="300" x="455"/>
        <rect width="60" height="300" x="520"/>
        <rect width="60" height="300" x="585"/>
        <rect width="60" height="300" x="650"/>
    </svg>

现在我们已经将矩形放置在视口中,并且它们随着窗口大小的增加和减少而正确地调整大小,这是通过我们的 viewBox 实现的。图 3.6 显示了不同窗口大小下的我们的 SVG(我们在 SVG 上添加了白色边框和条形,以便在屏幕截图中更明显)。内容在其可用空间内收缩和增长,而不会扭曲它们所包含的矩形,因为随着窗口大小的调整,宽高比发生变化。

图 3.6 在 SVG 内添加 11 个矩形

注意到我们的矩形是黑色的。我们接下来的任务是给它们添加样式。

3.3 将样式应用于 SVG

我们可以像在 HTML 中一样应用 SVG 元素的样式:内联、在 <style> 标签内部或在外部样式表中。然而,存在一些细微的差异。首先,SVG 如何导入到我们的 HTML 中会影响样式需要放置的位置以影响元素。

向网页添加矢量图形最简单的方法是使用图像标签。我们引用图像文件的方式与引用任何其他图像相同:<img src="myImage.svg" alt="">。我们也可以将其作为 background-image 添加到我们的 CSS 中:background-image: url("myImage.svg");

在这两种情况下,我们的 HTML 和样式可以影响 SVG,但不能影响其内部的元素。例如,我们可以影响图像的大小,但不能改变 SVG 中特定形状的颜色。图像基本上是一个黑盒,我们无法穿透以进行更改。要操纵图像内部的元素,我们必须将样式放置在 SVG 本身内部。

我们的第三个选项——我们将在本章中使用的是——将 SVG 的 XML 内联放置,直接在我们的 HTML 中而不是在外部文件中,防止我们遇到代码在外部文件中的黑盒问题。缺点是现在我们的图像代码与我们的 HTML 混合在一起,我们的关注点没有很好地分离。

当我们的 SVG 放置在 HTML 中时,应用于任何其他 HTML 元素的标准 CSS 应用方式同样适用。因此,我们可以将想要应用到我们的 SVG 中的样式放在我们的 CSS 中,就像 SVG 是任何其他 HTML 元素一样。

SVG 展示属性

在 HTML 中,当我们内联应用样式时,我们需要包含一个样式属性,例如 <p style="background: blue">。然而,SVG 有可以直接添加到元素作为属性的样式。这些样式被称为 展示属性

例如,fill 属性(SVG 中的 background-color 等价物),可以直接应用于元素而不需要样式标签:<rect fill="blue">。这些属性不必直接内联应用于元素。它们可以像应用任何其他 CSS 样式一样添加到样式标签或样式表中:rect { fill: blue; }

你可以在 mng.bz/Alee 找到 SVG 展示属性的完整列表。

尽管应用样式到我们的 SVG 元素的技术与 HTML 中的技术相同(除了上述内联应用的 SVG 展示属性),我们将用于样式化元素的某些属性将会有所不同。让我们更仔细地看看我们将在这个项目中使用的一个属性。

要设置加载条背景颜色,而不是使用 background-color,我们将使用 fill 属性,因为 background-color 属性对 SVG 元素不起作用。fill 属性支持与 background-color 相同的值,例如颜色名称、RGB(a)、HSL(a) 和十六进制。所以,我们不会写 rect { background-color: blue; },而是写 rect { fill: blue; }。如果未为特定形状分配 fill 值,则 fill 将默认为黑色,这就是为什么我们的矩形是黑色的。

让我们给我们的矩形添加一个填充颜色。因为不是所有的矩形都是同一种颜色(它们有不同深浅的蓝色和绿色,给加载器带来一点渐变效果),而不是给每个元素一个类,我们将使用伪类 nth-of-child(n),它根据元素在父元素中的位置来匹配元素。我们将寻找第 n 个矩形,我们将对其应用填充。因此,section rect:nth-of-type(3) 将找到容器中的第三个矩形。列表 3.5 展示了我们对每个矩形的填充颜色应用。

注意:伪类针对元素的状态——在这种情况下,其相对于其兄弟元素的位置。

列表 3.5 给我们的矩形添加填充颜色

rect:nth-child(1) { fill: #1a9f8c }
rect:nth-child(2) { fill: #1eab8d }
rect:nth-child(3) { fill: #20b38e }
rect:nth-child(4) { fill: #22b78d }
rect:nth-child(5) { fill: #22b88e }
rect:nth-child(6) { fill: #21b48d }
rect:nth-child(7) { fill: #1eaf8e }
rect:nth-child(8) { fill: #1ca48d }
rect:nth-child(9) { fill: #17968b }
rect:nth-child(10) { fill: #128688 }
rect:nth-child(11) { fill: #128688 }

图 3.7 显示了我们的输出。我们可以看到,加载器中的条不再为黑色;已经将颜色应用到它们上。

图片

图 3.7 填充应用到加载器矩形

我们声明的缺点是,如果另一个 SVG 图形有矩形,我们的代码可能会错误地样式化错误的图形。为了避免这个问题,我们可以在我们的 SVG 图形上添加一个类名作为标识符,以指定我们想要样式的矩形。但由于我们的项目中只有一个 SVG,我们不需要担心这个问题。

3.4 使用 CSS 动画元素

CSS 动画模块允许我们使用关键帧来动画化属性,我们将在 3.4.1 节中探讨这一点。我们可以控制动画的各个方面,例如它的持续时间以及它应该运行多少次。CSS 提供了几个我们可以用来定义动画行为的属性,包括以下内容:

  • animation-delay—动画开始前的等待时间

  • animation-direction—动画是向前播放还是向后播放

  • animation-duration—动画运行一次需要多长时间

  • animation-fill-mode—动画执行完成后,被动画化的元素应该如何样式化

  • animation-iteration-count—动画应该运行多少次

  • animation-name—应用的关键帧的名称

  • animation-play-state—动画是正在运行还是暂停

  • animation-timing-function—动画如何随时间通过样式

对于我们的动画,我们将关注以下四个属性:

  • animation-name

  • animation-duration

  • animation-iteration-count

  • animation-delay

我们想要创建的效果是矩形缩小和放大,但不是同步的。在任何给定的时间点,我们希望元素的高度略有不同。当矩形缩小和放大时,我们希望矩形的顶部和底部向中心移动,然后恢复到完整的高度。本质上,我们将创建一个挤压效果,从大到小再回到大。

尽管我们将对所有的矩形应用相同的动画,但为了错开它们的大小,我们将对每个矩形的动画开始应用一个稍微不同的延迟。由于每个矩形在不同的时间开始动画,每个矩形将处于不同的扩展和收缩阶段,从而产生涟漪效果。

首先,我们将创建动画本身。然后,我们将将其应用到矩形上。最后,我们将添加个别延迟以错开任何给定时间点的大小。为了创建动画,我们将使用关键帧。animation属性将引用关键帧并指定持续时间、延迟以及我们希望动画运行多少次。

3.4.1 关键帧和 animation-name

当我们创建关键帧时,我们需要给它一个名称。animation-name声明值与关键帧名称匹配以连接两者。使用animation-name属性,我们可以通过逗号分隔列出多个动画。

关键帧的起源

关键帧来自动画和电影行业。当公司以前手工制作动画时,艺术家会创作许多单独的图片,每张图片或帧中都有变化。随着时间的推移,他们在每一帧中进行更改,并逐渐达到最终帧。这种技术的简单例子是翻页动画。你拥有的帧越多,在短时间内进行的调整越微妙,动画就越流畅。

关键帧代表了动画中最重要(关键)的变化(即帧)。然后浏览器计算出定义帧之间的时间变化。这个过程被称为插值。允许硬件完成这项工作,浏览器可以快速填充关键帧之间的空隙,在一种状态和另一种状态之间创建平滑的过渡。插值过程如图 3.8 所示。

图片

图 3.8 插值

在 CSS 中,使用名为@keyframes的 at 规则定义关键帧,它控制动画序列中的步骤。at 规则是 CSS 语句,它规定了我们的样式应该如何表现以及何时应用。它们以一个 at(@)符号开头,后跟一个标识符(在我们的情况下是 keyframes)。我们在第二章中使用了 at 规则来创建我们的媒体查询;在这里,我们将使用它来创建关键帧。语法是@keyframes animation-name { ... }。大括号内的代码定义了动画的行为。@keyframes at 规则块内的每个关键帧都由一个百分比(动画中经过的时间百分比)和我们在那个时间点应用的风格定义。

在我们将动画应用到我们的项目之前,让我们先看看一个更简单的场景,以了解语法(列表 3.6)。你还可以在 CodePen 上找到这个例子,网址为codepen.io/michaelgearon/pen/oNyvbWX,在那里你可以看到动画的运行情况(图 3.9)。

图片

图 3.9 CodePen 中的简单动画场景

列表 3.6 示例动画

@keyframes changeColor {                                ①
  0% { background: blue }                               ①
  50% { background: yellow }                            ①
  100% { background: red }                              ①
}                                                       ①
@keyframes changeBorderRadius {                         ②
  from { border-radius: 0 }                             ②
  50% { border-radius: 50% }                            ②
  to { border-radius: 0 }                               ②
}                                                       ②
div {
  animation-name: changeColor, changeBorderRadius;      ③
  animation-duration: 3s;                               ④
  animation-iteration-count: 10;                        ⑤
}

① 第一个关键帧,命名为 changeColor

② 第二个关键帧,命名为 changeBorderRadius

③ 指向两个动画的 animation-name 属性

④ 设置动画完成所需的时间

⑤ 设置动画应该运行多少次

示例中有两组关键帧:一组命名为changeColor,另一组命名为changeBorderRadius。我们将这两个动画应用到div元素上。然后我们定义动画应该运行多长时间(3 秒)以及应该运行多少次(10 次)。在每一组关键帧内部,都有代码指定应该应用到元素上的样式。因此,我们有两种不同的表示法,我们有关键字,还有百分比。让我们分析一下我们在第一组关键帧中定义的内容。

我们断言,当动画开始时(0%),我们希望将<div>的背景色设置为蓝色。当我们达到动画的50%(3 秒的一半,即 1.5 秒)时,我们的背景将是黄色。当动画结束时(100%,或 3 秒),我们的背景将是红色。在关键帧之间,颜色会平滑地从一种状态变化到另一种状态。

在第二组关键帧中,changeBorderRadius,我们使用的是关键字fromto,而不是百分比。from等同于0%,而to等同于100%。我们可以在同一组关键帧中混合使用我们想要的符号。

当我们将动画应用于div规则集时,我们还设置了持续时间和迭代次数。请注意,这两个值被应用于两个动画。

在我们更详细地研究这两个属性及其工作原理之前,让我们为我们的加载器创建动画。对于我们的加载器,我们希望随时间增长和缩小——或者说缩放——我们的矩形。因此,我们将我们的关键帧命名为doScale。我们的 at 规则将是@keyframes doScale { }

在 at 规则内部,我们定义动画的关键帧。我们将从矩形具有完整高度开始。动画进行到一半时,我们希望矩形的高度是其原始高度的 20%。当动画结束时,我们希望矩形的高度恢复到完整大小。因此,我们需要定义三个步骤:from(或0%)、50%to(或100%)。

要改变矩形的大小,我们将使用transform属性,它允许我们改变元素的外观(旋转、缩放、扭曲、移动等),而不会影响周围的元素。如果我们使用height属性来减少元素的高度,下面的内容会向上移动以填充新可用的空间。使用transform,元素的空间和页面流中的位置不会改变——只有可见的方面会改变。使用相同的场景,如果我们使用transform来减少相同元素的高度,下面的内容不会向上移动。我们会有一个空白空间。

为了影响元素,transform属性接受一个transform()函数。我们将使用scaleY()。(您可以在mng.bz/Zo1N找到可用的完整函数列表。)

scaleY()函数垂直调整元素的大小,而不会影响其宽度或挤压或拉伸它。为了定义元素应该挤压或拉伸的程度,我们向函数传递一个百分比或数值。数值映射到其百分比等价的十进制值;因此,scaleY(.5)scaleY(50%)达到相同的结果,将元素的高度减少到原始值的 50%。大于100%的值会增加元素的大小,而介于0%100%之间的值会缩小它。

scaleY() 应用的负值会垂直翻转元素,因此 scaleY(-0.5) 会将元素翻转过来并使其高度缩小 50%。scaleY(-1.5) 会将元素翻转过来,并使其高度变为原始值的 1.5 倍。

对于我们的加载条,我们希望矩形在动画开始和结束时达到满高度,在动画中途达到原始高度的 20%。我们应用了变换的完成关键帧如下所示。

列表 3.7 完成关键帧

@keyframes doScale {                   ①
  from { transform: scaleY(1) }        ②
  50% { transform: scaleY(0.2) }       ③
  to { transform: scaleY(1) }          ④
}                                      ⑤
rect { animation-name: doScale; }      ⑥

doScale at-rule 的开始

② 在全高度处开始动画

③ 动画中途,高度应该是原始值的 20%。

④ 到动画结束时,矩形返回到满高度。

doScale at-rule 的结束

⑥ 将动画应用于矩形

如果我们运行代码,我们会注意到没有任何变化;我们的矩形还没有增长和缩小,尽管我们已经为矩形应用了关键帧。我们仍然需要定义持续时间和迭代次数。让我们进一步探讨这些属性。

3.4.2 持续时间属性

duration 属性设置动画从开始到结束的持续时间。持续时间可以用秒(s)或毫秒(ms)来设置。持续时间越长,动画完成得越慢。考虑到可访问性,我们希望考虑对运动敏感的用户(第 3.4 节)并选择一个合理的持续时间。

动画、抽搐和闪烁率

互联网联盟(W3C)建议,为了防止在光敏感用户中引起抽搐,我们需要确保我们的动画在任何 1 秒期间内不会闪烁超过三次(mng.bz/RldR)。

选择合适的动画时间需要考虑很多因素。动画过快可能会产生难以察觉的变化或引起抽搐,这取决于其性质。动画过慢可能会使我们的应用程序看起来反应迟缓。大多数微动画都很短,是过渡性的;它们动画化元素从一个状态到另一个状态的变化,例如将箭头从向上指变为向下指。这类动画的普遍接受的时间大约是 250 毫秒。

如果动画更大或更复杂,例如打开和关闭一个大的面板或菜单,我们可以将持续时间增加到大约 500 毫秒。然而,加载器略有不同。它不是对用户动作的快速反应;它是一个用户会关注一段时间的大型视觉元素。

在确定加载器的“正确”时间时,我们通常使用试错法来找到与我们的图形最匹配的速度。对于我们的项目,我们希望将动画设置为在 2.2 秒内完成。为了应用动画应该持续的时间,我们在矩形上添加了 animation-duration 属性,如下所示。

列表 3.8 添加动画持续时间

rect {
  animation-name: doScale;
  animation-duration: 2.2s;
}

当我们运行代码时,我们的加载器动画只运行一次,然后永远不会再次动画化,除非我们重新加载浏览器窗口。我们还注意到所有条形同时增加和减少大小。首先,让我们让我们的加载器随着时间的推移继续动画化;然后我们将动画错开在我们的矩形之间,使它们看起来有不同的高度。

3.4.3 iteration-count属性

为了让我们的动画在完成之后重新开始,我们使用iteration-count属性,该属性设置动画应该重复的次数。默认情况下,其值为1。因为我们还没有设置值,浏览器假设我们想要动画只运行一次然后结束。我们希望我们的动画能够持续重复,所以我们将使用infinite关键字值。

通过应用这个值,我们声明动画应该永远播放。如果我们想要运行特定的次数,我们将使用一个整数值。在我们添加迭代计数后,我们的代码看起来如下所示。

列表 3.9 添加动画迭代计数

rect {
  animation-name: doScale;
  animation-duration: 2.2s;
  animation-iteration-count: infinite;
}

当我们运行代码时,我们看到所有矩形同步地增长和缩小,从顶部开始,并且动画在完成后会重新开始。我们还有一些工作要做,将动画设置在矩形的中间而不是顶部,以及使动画在我们的元素之间错开。不过,首先让我们快速暂停一下,看看动画简写属性。

3.4.4 animation简写属性

我们目前有三个声明定义了我们的动画:animation-nameanimation-durationanimation-iteration-count。我们可以通过将这三个声明组合在animation简写属性中来简化我们的代码,这允许我们使用单个属性定义动画的行为。在这个属性中,我们可以定义 3.3 节中列出的任何属性的值。我们不需要为所有属性提供值。如果属性没有作为简写属性或单独定义,它们将使用默认值。

如前所述,我们正在定义三个属性:animation-nameanimation-durationanimation-iteration-count。重构后使用动画简写属性,我们的声明看起来如图 3.10 所示。

图片

图 3.10 动画简写属性的分解

这段代码在功能上与目前应用于我们矩形的代码相同。使用简写属性可以使我们的代码更简洁,并且可以使其更容易阅读。但如果你发现逐个写出每个属性对你来说更容易,两种方法都是完全有效的。做对你最有利的事情。

当我们使用动画简写属性时,我们的更新后的 CSS 看起来如下所示。在修改我们的代码后,我们注意到我们的动画没有改变。

列表 3.10 使用简写属性重构

rect {
 animation-name: doScale;
 animation-duration: 2.2s;
 animation-iteration-count: infinite;
  animation: doScale 2.2s infinite;
}

接下来,让我们解决每个矩形的错开高度问题。

3.4.5 动画延迟属性

animation-delay 属性的作用正如其名称所暗示的那样:它允许我们在元素上延迟动画。延迟应用于动画的开始。当动画开始时,它将正常循环。与 duration 属性一样,我们可以使用秒(s)或毫秒(ms)来设置延迟的持续时间值。默认值是 0。默认情况下,动画没有延迟。

为了在我们的动画中创建错位效果,我们将为我们的每个矩形分配不同的延迟值,如列表 3.11 所示。第一个矩形的动画将立即开始。我们给它一个延迟值 0。我们可以省略这个声明,因为 0animation-delay 的默认值;我们在这里添加它是为了使代码更清晰。

第二个矩形有一个 200ms 的延迟,并且我们继续为每个后续的矩形增加 200ms 的延迟。注意,在第六个矩形上,我们改为使用秒而不是毫秒。我们这样做是为了使代码更易读,因为秒或毫秒值都是可接受的。

列表 3.11 添加了动画迭代次数

rect:nth-child(1) {
  fill: #1a9f8c;
  animation-delay: 0;
}
rect:nth-child(2) {
  fill: #1eab8d;
  animation-delay: 200ms;
}
rect:nth-child(3) {
  fill: #20b38e;
  animation-delay: 400ms;
}

rect:nth-child(4) {
  fill: #22b78d;
  animation-delay: 600ms;
}
rect:nth-child(5) {
  fill: #22b88e;
  animation-delay: 800ms;
}
rect:nth-child(6) {
  fill: #21b48d;
  animation-delay: 1s;
}
rect:nth-child(7) {
  fill: #1eaf8e;
  animation-delay: 1.2s;
}
rect:nth-child(8) {
  fill: #1ca48d;
  animation-delay: 1.4s;
}
rect:nth-child(9) {
  fill: #17968b;
  animation-delay: 1.6s;
}
rect:nth-child(10) {
  fill: #128688;
  animation-delay: 1.8s;
}
rect:nth-child(11) {
  fill: #128688;
  animation-delay: 2s;
}

在添加延迟后,我们可以看到我们实现了错位效果(图 3.11)。但是元素是从顶部而不是从中心开始增长和缩小的。

图片

图 3.11 从顶部发出高度变化的动画矩形

要说明我们希望元素从哪里开始增长和缩小,我们需要告诉浏览器动画应该在矩形的哪个位置开始。为了解决这个问题,我们将使用 transform-origin 属性。

3.4.6 变换原点属性

transform-origin 属性设置元素变换的起点或点。如果我们旋转对象,transform-origin 属性将设置我们想要从元素上的哪个位置旋转。在我们的情况下,我们将使用此属性来设置动画应该开始的位罝(原点)。

如果变换发生在三维(3D)空间中,值可以有三个(xyz);如果变换发生在二维(2D)空间中,我们可以有两个值(xy)。第一个值是水平位置,或 x 轴;第二个值是垂直位置,或 y 轴。当我们处于 3D 空间工作时,第三个值将是前后方向,或 z 轴。

我们可以用三种方式声明 transform-origin 属性的值:

  • 长度

  • 百分比

  • 关键词

    • top

    • right

    • bottom

    • left

    • center

在 HTML 中,此属性的初始值是 50% 50% 0,即 centercenterflat。然而,对于 SVG 元素,初始值是 0 0 0,这将其放置在左上角。

对于我们的动画,我们希望矩形的变换原点位于中心。我们希望矩形的上下部分缩小,而不是顶部固定,然后矩形从这个点开始扩展和收缩。为此,我们可以应用关键字值center,或者将transform-origin属性的值设置为50%。无论哪种方式,我们都在说我们希望原点位于矩形的中心。对于我们的项目,我们将使用关键字值center。列表 3.12 显示了我们的更新后的rect规则。

我们之前提到,当与 2D 动画一起工作时,该属性需要两个值,但我们只传递了一个。当只传递一个值时,它应用于垂直和水平位置;因此,transform-origin: center; 等同于 transform-origin: center center;

列表 3.12 使用transform-origin属性的更新后的rect规则

rect {
  animation: doScale 2.2s infinite;
  transform-origin: center;
}

我们已经完成了加载动画(图 3.12)。但我们仍然需要考虑我们的设计如何具有可访问性。第 3.4 节深入探讨了我们可以为所有用户提供积极体验的一些方法。

图片

图 3.12 完成的加载动画

3.5 可访问性和prefers-reduced-motion媒体查询

随着这些效果变得更容易实现和浏览器支持改进,网页上运动、视差(背景比前景移动慢的效果)和动画的使用已经增加。通过使用这些技术,我们可以创建更丰富的用户界面,它们是交互式的,并提供更丰富的用户体验。

然而,使用这些技术的代价是。对于一些用户,特别是那些有前庭功能障碍的用户,屏幕上的运动可能引起头痛、头晕和恶心。正如我们之前提到的,动画也可能引起癫痫发作,特别是如果它们包含闪烁元素的话。

在许多操作系统中,用户可以在他们的设备上禁用动画。在我们的应用程序中,我们需要确保我们尊重那些偏好。为了检查用户设置,第 5 级媒体查询模块引入了prefers-reduced-motion媒体查询。这个查询是一个 at-rule,它检查用户对屏幕上运动的偏好,并允许我们根据这些偏好应用条件样式。查询有两个值:

  • no-preference

  • reduce

当用户偏好减少运动或未指定偏好时,我们可以选择禁用或减少动画。用户的减少运动偏好并不意味着我们不能使用任何动画,但我们应该选择性地保留哪些动画。可能决定保留哪些动画启用的方面包括

  • 它有多快

  • 它有多长

  • 它占视口的多少

  • 闪烁频率是多少

  • 它对于网站功能或内容理解有多重要

TIP 值得注意的是,用户可能更喜欢减少或没有动画,但可能不知道如何通过系统首选项设置来关闭动画。提供一个现场退出按钮可能是有用的,这取决于我们的网站有多少动画。

动画的可访问性指南

用户应该能够暂停、停止或隐藏持续时间超过 3 秒且不被认为是基本内容的动画(mng.bz/RldR)。在这一点上,加载器有点棘手,因为它们向用户传达重要信息(应用程序正在做某事并且没有冻结),但可能很大并且有很多动作。

我们的加载器可以被认为是基本内容,但我们还提供了一个进度条在其下方,以向用户指示应用程序正在做什么。因为信息是通过不同的媒介传达的,并且因为动画很大,有很多动作,并且可能持续超过 3 秒,我们将为更喜欢减少运动的用户禁用它,使用以下列表中的代码。

列表 3.13 为更喜欢减少运动的用户禁用动画

@media (prefers-reduced-motion: reduce) {     ①
  rect { animation: none; }                   ②
}

① 当用户启用 prefer-reduced-motion 首选项时,条件性地应用 at 规则内的样式

② 禁用之前应用于矩形的动画

要检查我们是否成功禁用了动画,而不是编辑我们机器的设置,在大多数浏览器中我们可以做以下操作:

  1. 进入我们的浏览器开发者工具。

  2. 在控制台标签显示中,选择渲染标签。

    (在 Google 的 Chrome 浏览器中,如果此标签尚未显示,请点击垂直省略号按钮,然后从下拉菜单中选择更多工具 > 渲染。)

  3. 启用减少运动模拟。

图 3.13 展示了在此写作时 Chrome 最新版本中禁用的动画和开发者工具(mng.bz/51rZ)。

图 3.13 使用 Chrome 开发者工具模拟减少运动首选项

在我们的加载动画完成并且可访问性需求得到处理后,让我们将注意力转向屏幕底部的进度条。

3.6 为 HTML 进度条设置样式

<progress> HTML 元素可以用来显示某物正在加载或上传,或者数据已经被传输。它通常用来显示用户完成了多少任务。

<progress>元素的默认样式在不同浏览器和操作系统之间有所不同。进度条的大部分功能在操作系统级别处理;因此,我们可用的属性很少,可以重新设计控件,尤其是在条内彩色进度指示器方面。在本节中,我们将探讨一些解决方案及其陷阱。让我们从一个简单的开始。

图 3.14 显示了由列表 3.14 中的 HTML 生成的起点。在此阶段,尚未应用到控件上的样式。该图显示了 Martine 的机器生成的默认样式。

图 3.14 Chrome 中进度条的起点

列表 3.14 进度条 HTML

<body>
  <section>
    ...
    <progress value="32" max="100">32%</progress>     ①
  </section>

① 进度条

3.6.1 进度条样式

让我们从改变高度和宽度开始。为了将进度条的宽度增加到与部分宽度相匹配,我们将它的width属性值设置为100%。我们还想将高度增加到24px

要改变进度指示器(控件的有色部分)的颜色,我们可以使用一个相当新的属性:accent-color。这个属性允许我们改变表单控件的颜色,如勾选标记、单选输入和progress元素。我们将它设置为#128688,与我们的加载器最后一条条的颜色相匹配。以下列表显示了我们的进度规则到目前为止的情况。

列表 3.15 进度规则

progress {
  height: 24px;
  width: 100%;
  accent-color: #128688;
}

图 3.15 显示了列表 3.15 中应用于我们的控件的样式。

图 3.15 应用于progress元素的宽度、高度和突出颜色

如果我们尝试给我们的元素添加背景颜色(background: pink),我们会注意到添加并没有起作用。事实上,它失败了(见图 3.16)。它彻底改变了元素的外观,并改变了我们之前设置的accent-color。此外,背景颜色变成了灰色而不是粉色。

图 3.16 background-color失败

我们如何解决这个问题?为了重新设计控件样式,我们需要忽略默认样式并从头开始重新创建样式。然而,要做到这一点,我们需要使用供应商前缀属性。

供应商前缀

从历史上看,当浏览器引入新属性时,它们会在属性名之前添加供应商前缀。每个浏览器的前缀都基于它所使用的渲染引擎。表 3.1 显示了主要浏览器及其前缀。

表 3.1 供应商前缀及其浏览器

前缀 浏览器
-webkit- Chrome, Safari, Opera, 大多数 iOS 浏览器(包括 iOS 的 Firefox),Edge
-moz- Firefox

供应商前缀通常是浏览器可能随时选择移除或重构的不完整或不标准的实现。尽管这个事实已经明确记录了多年,但那些渴望使用最新属性的开发商仍然经常在生产环境中使用它们。

为了防止这种行为持续发生,大多数主要浏览器都转向在功能标志后面发布实验性功能。要启用该功能并与之互动,用户必须进入他们的浏览器设置并启用该特定标志。

通过转向基于标志的方法,浏览器能够让开发者尝试实验性、前沿的功能,而不用担心非标准实现可能会被用于生产代码中。但许多供应商前缀属性仍然在野外可用。有关供应商前缀和功能标志的更多信息,请参阅附录。

我们要做的第一件事是修复我们的 background-color 问题,即移除控件默认的 appearance。

appearance 属性

为了重置 <progress> 元素的 appearance,我们使用 appearance 属性。通过将其值设置为 none,我们取消由用户代理提供的默认样式。因为我们将从零开始创建所有样式,所以我们可以移除 accent-color 属性,因为它将不再有任何效果。

我们将保持高度和宽度,并添加一个 border-radius,因为我们将会有一个曲线的结束效果。appearance 属性被所有主要浏览器的所有新版本支持,但我们仍然需要包含供应商前缀版本,因为我们将使用的一些实验性属性需要它们。以下列表显示了我们的更新规则。

列表 3.16 更新进度规则

progress {
  height: 24px;
  width: 100%;
  border-radius: 20px;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

在这一点上,我们的进度条看起来与我们通过添加背景颜色而破坏它时相同。这个结果是预期的。通过添加 appearance:none,我们可以开始以我们之前无法做到的方式改变控件。首先,我们将关注带有 -webkit- 前缀的浏览器。

3.6.2 为 -webkit- 浏览器设置进度条样式

我们可以使用三个供应商前缀的伪元素来编辑我们的进度条样式:

  • ::-webkit-progress-inner-element—进度元素的 最外层部分

  • ::-webkit-progress-bar—进度元素的全部条,进度指示器下方的部分,以及 ::-webkit-progress-inner-element 的子元素

  • ::-webkit-progress-value—进度指示器和 ::-webkit-progress-bar 的子元素

我们将使用所有三个伪元素来设置我们的元素样式。让我们从内部开始,逐步向外。我们想要设置的第一部分是进度指示器,我们需要使用 ::-webkit-progress-value。我们将边缘弯曲并将条的颜色更改为浅蓝色,如下列所示。

列表 3.17 在 Chrome 中设置进度指示器样式

::-webkit-progress-value {
  border-radius: 20px;
  background-color: #7be6e8;
}

图 3.17 显示了在 WebKit 浏览器中的输出。

图片

图 3.17 Chrome 中设置的进度值

接下来,我们将通过使用 ::-webkit-progress-bar 编辑进度指示器后面的背景。我们还将添加圆角并将颜色更改为线性渐变,从深绿色渐变到浅蓝色,以符合整个作品的主题。

linear-gradient() 函数接受一个方向,后跟一系列颜色和百分比的配对。方向决定了渐变的角;颜色-百分比配对决定了我们在渐变中想要从一种颜色切换到另一种颜色的点。我们将使用关键字值 to right 作为我们的方向。然后我们将起始颜色设置为 #128688,结束颜色设置为 #4db3ff。因此,我们的渐变将从左到右,从起始颜色渐变到结束颜色。

CSS 渐变生成器和供应商前缀

由于渐变手动编写可能很繁琐,因此已经创建了多个 CSS 渐变生成器,它们在网络上免费提供。许多生成器仍然在其生成的代码中包含供应商前缀。由于现在所有主流浏览器都支持渐变,并且需要这些前缀的浏览器几乎已经完全不存在,因此这些前缀不再是必要的。

最后,我们给最外层的容器添加一个边框半径。我们的进度条 CSS 如下所示。

列表 3.18 Chrome 中样式化进度指示器容器

::-webkit-progress-bar {
  border-radius: 20px;
  background: #4db3ff;                         ①
  background: linear-gradient(to right, #128688 0%,#4db3ff 100%);
}
::-webkit-progress-inner-element {
  border-radius: 20px;
}

① 渐变的后备颜色

我们在 Chrome 中的进度指示器看起来很棒(图 3.18)。接下来,让我们看看它在 Firefox 中的样子。

图 3.18 Chrome 中的样式化进度指示器

在 Firefox 中(图 3.19),我们发现我们的控件保持相对无样式,因为它需要的是 -moz- 前缀,而不是 -webkit- 前缀。因为我们已经为 -webkit- 前缀编写了代码,所以我们需要为使用 -moz- 前缀的浏览器做同样的事情。

图 3.19 Firefox 中的无样式进度条

3.6.3 为 -moz- 浏览器样式化进度条

对于 Firefox,我们将采取不同的样式方法,因为我们没有太多属性可以操作。我们可用的唯一 -moz- 前缀属性是 ::-moz-progress-bar。它也是一个伪元素,它针对进度指示器本身。因此,我们将以与 Chrome 中 ::-webkit-progress-value 相同的方式对其进行样式化,因为我们希望在两个浏览器中达到相同的外观。

由于我们使用的是相同的样式,因此将 -moz- 选择器添加到现有规则中是合理的:::-moz-progress-bar, ::-webkit-progress-value { ... }。它在 Firefox 中(图 3.20)工作得很好,但它会破坏 Chrome(图 3.21)。

图 3.20 Firefox 中的样式化进度条

图 3.21 在同一个规则中添加两个选择器会破坏 Chrome。

在同一个规则中包含多个选择器不应该导致这种副作用,但我们正在处理实验性属性,它们有时会有非标准的行为。为了防止这种不幸的副作用,我们将为每个选择器编写两个相同的规则,如下面的列表所示。

列表 3.19 Chrome 中样式化进度指示器容器

::-webkit-progress-value {      ①
  border-radius: 20px;          ①
  background-color: #7be6e8;    ①
}
::-moz-progress-bar {           ②
  border-radius: 20px;          ②
  background-color: #7be6e8;    ②
}

① Chrome 的规则

② Firefox 的规则

要改变 Firefox 的背景颜色,我们需要给进度元素本身添加一个背景属性值。我们使用与 ::-webkit-progress-bar 规则中相同的渐变。图 3.22 显示了 Firefox 中的进度。

图 3.22 Firefox 中应用了背景的进度元素

我们最后需要做的是移除边框,我们将应用它到 progress 规则上。为了达到这个效果,我们将边框属性值设置为 none。下面的列表显示了我们的最终进度规则。

列表 3.20 最终进度规则

progress {
  height: 24px;
  width: 100%;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  border-radius: 20px;
  background: linear-gradient(to right, #128688 0%,#4db3ff 100%);    ①
  border: none;                                                      ②
}

① 渐变背景

② 移除边框

正如我们在图 3.23 中所看到的,我们在 Chrome 和 Firefox 中实现了相同的结果。

图片

图 3.23 Firefox 中完成的进度条样式

我们必须强调,这些样式是通过使用非标准且可能在未来更改的实验性功能实现的。这里的价值在于能够在新功能变得易于获取之前进行实验。这也是参与社区的机会;在开发浏览器功能和规范的工作组在新的标准被接受并推广到一般使用之前请求反馈的情况并不少见。

摘要

  • animation 属性是一种使用 CSS 动画化位置、颜色或其他视觉元素值的方法。

  • @keyframes 规则是一种定义动画关键帧的方法。

  • 我们可以通过使用 animation-delay 属性来延迟动画的开始。

  • animation-duration 设置动画单次迭代完成所需的时间。

  • SVG 可以使用 CSS 进行样式化。

  • prefers-reduced-motion 媒体查询允许我们根据用户的设置有条件地样式化动画。

  • HTML 进度条是一种显示已加载内容多少的方法。

  • 默认情况下,浏览器会应用自己的样式到进度条上,但可以通过使用 appearance 属性并设置值为 none 来重置它。

  • 除非我们使用实验性属性,否则我们对 progress 元素的样式化能力相当有限。

  • 一些非标准属性可用于样式化 progress 元素,但它们需要使用供应商前缀。供应商前缀的属性是实验性的,这意味着它们有时会有非标准的实现,并且可能会随时更改。

4 创建响应式网络报纸布局

本章涵盖

  • 使用 CSS 多列布局模块创建报纸布局

  • 使用 counter-style CSS 规则创建自定义列表样式

  • 使用 filter 属性样式化图片

  • 处理损坏的图片

  • 格式化标题

  • 使用 quotes 属性向 HTML 元素添加引号

  • 使用媒体查询根据屏幕大小更改布局

在第一章中,我们探讨了创建单列文章,这使我们了解了 CSS 的基本原理。然而,设计很简单。让我们重新审视文章格式化的概念,但要使其更具视觉吸引力。在本章中,我们将样式化我们的内容,使其看起来像报纸的一页,如图 4.1 所示。

图 4.1 我们想要达到的结果

要创建内容列,我们将使用 CSS 多列布局模块。在这个过程中,我们还将探讨如何管理列之间的空间,如何跨列扩展元素,以及如何控制内容如何断行到新列。

报纸页面的一部分使用了一个项目列表,它由用户代理(UA)样式表为我们提供的默认样式。我们将探讨如何使用 CSS 列表和计数器模块,它允许我们自定义我们的 list-items 计数器(数字和项目符号)的样式。

本章我们将探讨的另一个概念是如何样式化图片,包括使用 filter 属性和函数来改变图片的外观。我们还将探讨解决损坏图片的方法以及使它们优雅失败的方式。当我们说“优雅失败”(有时也称为 优雅降级)时,我们正在设置回退,以便在我们要加载的东西出现问题或我们要使用的功能与用户的浏览器不兼容时使用。

您可以在 GitHub 仓库的 chapter-04 文件夹中找到我们项目的代码(mng.bz/OpOa)或 CodePen 上的 codepen.io/michaelgearon/pen/yLxzbr。我们的起始 HTML 由列表 4.1 中的元素组成。在 <body> 元素中是报纸的标题和印刷日期,然后是一篇文章。文章有一个标题、作者姓名、引言、两个副标题、一个列表、一些段落和一张图片。

列表 4.1 起始 HTML

<body>
  <h1>Newspaper Title</h1>                                         ①
  <time datetime="2021-09-07">                                     ②
    Tuesday, 5<sup>th</sup> September 2021                         ②
  </time>                                                          ②
  <article>                                                        ③
    <h2>Article heading</h2>                                       ④
    <div class="author">John Doe</div>                             ⑤
    <p>Maecenas faucibus mollis interdum. Cum sociis nato...</p>
    <p>Integer posuere erat a ante venenatis dapibus posu...</p>
    <blockquote>                                                   ⑥
      Fusce dapibus, tellus ac cursus commodo, tortor ma...        ⑥
    </blockquote>                                                  ⑥
    <p>Aenean lacinia bibendum nulla sed consectetur. Dui...</p>
    <h3>Subheading</h3>                                            ⑦
    <ul>                                                           ⑧
      <li>List item 1</li>                                         ⑧
      ...                                                          ⑧
    </ul>                                                          ⑧
    <p>Cras justo odio, dapibus ac facilisis in, egestas ...</p>
    <p>Donec ullamcorper nulla non metus auctor fringilla...</p>
    <h3>Subheading</h3>                                            ⑨
    <img src="./image.jpg" alt="">                                 ⑩
    <p>Praesent commodo cursus magna, vel scelerisque nisl...</p>
    <p>Morbi leo risus, porta ac consectetur ac, vestibulu...</p>
  </article>                                                       ⑪
</body>
</html>

① 报纸标题(主标题)

② 印刷日期

③ 文章开始

④ 文章标题

⑤ 文章作者

⑥ 引言

⑦ 第一个副标题

⑧ 列表

⑨ 第二个副标题

⑩ 图片

⑪ 文章结束

图 4.2 显示了我们的起始点。应用于 HTML 的样式是浏览器提供的默认样式。页面尚未应用任何作者样式。

图 4.2 起始点

在我们担心布局之前,让我们定义我们的主题。

4.1 设置我们的主题

主题为页面设定基调;它通常包括颜色、字体、边框,有时还包括填充。我们的主题将保持不变,无论屏幕大小或布局如何。通常,网站的主题与其标志和品牌颜色紧密相关。

我们将在 <body> 元素上设置一些默认值,这些值可以被其后代继承。作为一般规则,围绕排版(colorfont-family 等)的样式可以被大多数元素继承。例外是一些表单元素,我们将在第十章中介绍。当我们设置父级的可继承属性时,样式会向下传递到后代,从而减轻了我们需要将它们应用到每个元素上的需求。

4.1.1 字体

我们应用背景颜色、字体和文字颜色(列表 4.2)。注意,在 body 规则之前,我们从 Google Fonts 导入我们选择的 font-family。Google Fonts (fonts.google.com) 是开发者中流行的选项,因为它免费提供,用户无需创建账户或担心许可问题。

警告:当从内容分发网络(CDN)加载库或资产,包括字体时,始终检查隐私和数据条款,并确保它们符合当地法律,如通用数据保护条例(GDPR)和欧盟法律。如有疑问,请咨询您的法律团队。如果您没有 CDN 选项,请查看第九章以获取有关本地加载字体的详细信息。

例如,PT Serif 并不是我们期望用户已经在他们的电脑上加载的字体;因此,我们必须导入它,以便浏览器知道 glyphs(字母、数字和符号)应该看起来像什么。我们还提供了一个默认的 serif 作为回退选项,以防导入失败。

网络安全字体

可用的网络安全字体(我们可以假设大多数设备都能访问到的字体)只有少数。根据 W3Schools (mng.bz/Y6Ea),一些安全选项包括 Arial、Verdana、Helvetica、Tahoma、Trebuchet MS、Times New Roman、Georgia、Garamond、Courier New 和 Brush Script MT。但没有官方标准规定构成网络安全字体或哪些字体会在所有浏览器和设备上真正可用。因此,无论我们选择哪种字体族,始终提供回退值(serifsans-serifmonospacecursivefantasy)是一个好习惯。

虽然我们将在本章后面部分进行大量布局,但现在我们将在主体上添加一些左右填充,以便将文本从边缘移开。

列表 4.2 定义一些主题样式

@import url('https:/ /fonts.googleapis.com/css2?family=PT+Serif&display=swap'); ①

body {
  background-color: #f9f7f1;
  font-family: 'PT Serif', serif;                                               ②
  color: #404040;
  padding: 0 24px;
}

① 从 Google Fonts 导入 PT Serif

② 将 PT Serif 应用于我们的内容并提供回退选项

图 4.3 展示了我们的更新页面。注意,<body> 中的所有元素都继承了 colorfont-family

图 4.3

图 4.3 主题样式应用于主体并继承到后代

接下来,我们将对主标题和副标题进行样式设计。让我们从报纸标题开始,它在 HTML 中是 <h1>。我们希望将 font-family 改为使用名为 Oswald 的字体,增加文本大小,使其加粗,将字体转换为全大写字母,设置行高,并使文本居中。像 PT Serif 一样,Oswald 并不是大多数用户设备上都能识别的字体,所以我们就像导入 PT Serif 一样导入它。

注意,对于文本大小,我们使用单位 rem,它代表“根 em”。一个 em 是基于元素父字体大小的相对单位。如果一个容器 div 的字体大小为 12px,我们将子元素的大小设置为 .5em,则子元素的大小将是 12 x .5 或 6pxrem 单位的工作方式类似,但它不是相对于父元素的字体大小,而是基于根元素的基础值——在我们的例子中,是 <html>。我们没有在 HTML 元素上设置字体大小;因此,我们的基础将是浏览器的默认值,在大多数情况下是 16px。考虑到这一点,4rem 的字体大小——我们设置在主标题上的大小——将相当于 4 x 16 或 64px

要从 Google Fonts 导入 Oswald,我们可以在文件顶部添加第二个 @import,或者为了更好的性能,我们可以将两个导入合并为一个 @import 语句。合并两个导入的能力是 Google Fonts 特有的;并非所有 CDN 都有这种能力。

注意在列表 4.3 中,在我们的 @import 之后,我们看到 :wght@400;700。此代码表示我们想要导入哪些 Oswald 字体权重。

列表 4.3 样式化报纸标题

@import url('https:/ /fonts.googleapis.com/css2?
➥ family=Oswald:wght@400;700&family=PT+Serif&display=swap');     ①

h1 {
  font-weight: 700;                                               ②
  font-size: 4rem;
  font-family:'Oswald', sans-serif;
  line-height: 1;
  text-transform: uppercase;
  text-align: center;
}

① 包含 Oswald 和 PT Serif 的更新导入

② 等同于使用粗体值

图 4.4 显示了我们的更新后的标题。

图 4.4 样式化标题

4.1.2 字体权重属性

font-weight 属性可以接受介于 100900 之间的数字值或关键字值(normalboldlighterbolder)。normal 等同于 400,而 bold 等同于 700lighterbolder 会根据父元素的字体权重来改变元素的字体权重。表 4.1 显示了数字 font-weight 值与其常见名称等效关系。

表 4.1 font-weight 值及其常见名称

常见名称
100 薄(细线)
200 额外轻量(超轻量)
300 轻量
400 正常(常规)
500 中等
600 半粗体(半粗)
700 粗体
800 额外粗体(超粗体)
900 黑色(粗体)
950 额外黑色(超黑色)

如果我们没有导入与规则中设置的权重相匹配的权重,浏览器将应用它所能访问的最接近的权重。因此,如果我们只以400的权重导入 Oswald,并将我们的元素应用font-weight值为bold,浏览器将显示我们的文本为400的权重,因为这个值是它唯一可以工作的值。

4.1.3 字体简写属性

使用font简写属性,我们可以将规则中的大多数样式组合在一起。font属性要求我们提供一个font-familysize,可选地后跟stylevariantweightstretchline-height,使用以下语法:font: font-style font-variant font-weight font-stretch font-size/line-height font-family。下一个列表显示了使用font的更新规则。

列表 4.4 使用font简写属性设置标题样式

h1 {
  font: 700 4rem/1 'Oswald', sans-serif;
  text-transform: uppercase;
  text-align: center;
}

让我们将关于导入字体、font-weightfont简写属性的概念应用到样式化文章的主要标题和副标题。

4.1.4 视觉层次结构

为了在页面上创建视觉层次结构,我们将文章标题<h2>设置为比报纸的主要标题<h1>小,但比文章内的副标题<h3>大。一般来说,元素越大,人们认为它越重要,所以我们使用大小来使标题突出。通过使用与正文文本不同的font-family,并将所有标题字母转换为大写,我们进一步区分了它们。

创建视觉层次结构很重要,因为它使用户能够快速浏览屏幕并立即识别感兴趣元素。它还将信息分成组,使信息更容易处理和理解。

列表 4.5 显示了我们的标题规则。我们将保持相同的字体族,将字母转换为大写,并调整大小。我们还将移除文章标题的浏览器提供的底部边距,以使它们更接近它们之前的文本。

列表 4.5 文章标题规则

h2 {                                     ①
  font: 3rem/.95 'Oswald', sans-serif;
  text-transform: uppercase;
  margin-bottom: 16px;
}

h3 {                                     ②
  font: 2rem/.95 'Oswald', sans-serif;
  text-transform: uppercase;
  margin-bottom: 12px;
}

① 文章标题

② 文章副标题

现在我们文章的标题看起来像图 4.5。

图 4.5 样式化的文章标题

4.1.5 内联元素与块元素

让我们继续使重要元素从其余内容中脱颖而出,从出版日期开始,该日期位于我们的 HTML 中的<time>元素内。<time>元素在语义上表示特定的时间段;它接受一个可选的datetime属性,该属性以机器可读的格式提供日期,以便搜索引擎搜索。我们的<time>元素看起来像这样:<time datetime="2021-09-07">Tuesday, 5<sup>th</sup> September 2021</time>。图 4.6 显示了我们要达到的外观。

图 4.6 样式化的出版日期

从字体排印开始,我们使文本居中,使用 Oswald 字体家族,将font-size设置为1.5rem,并将文本改为大写和粗体。然后我们将上标元素(<sup>)中找到的*th的文本大小改为略小的字体大小和正常重量,以降低其突出度。

接下来,我们添加顶部和底部边框,使其为 3 像素粗细的实心深灰色线条。添加边框后,我们添加一些顶部和底部填充,以便在文本和边框之间留出一些空间。

<time>元素是一个内联级元素,这意味着它只占据其内容所需的确切空间,就像<span><a>元素一样。

相比之下,块级元素(如<div><p><ul>)将自己放置在新的一行上,并占据其可用空间的全宽,除非指定了固定宽度。为了实现图 4.6 中的设计,我们希望<time>元素表现得像块级元素,这样文本就会放置在屏幕中间,边框将占据整个页面的宽度。

图片

图 4.7 <time>元素表现出内联行为

要更改元素默认行为,我们将使用display属性并为其提供一个值为block的值。图 4.7 和图 4.8 显示了在添加display属性之前和之后<time>元素的表现。在图 4.7(添加display属性之前),元素表现出其作为内联级元素的默认行为。在图 4.8(添加display属性之后),元素表现得像块级元素,占据整个屏幕的宽度。

图片

图 4.8 <time>元素表现出块级行为

以这种方式样式化出版日期有两个目的:样式使其突出,并在报纸信息(日期和报纸的主要标题)与文章本身(日期以下的所有内容)之间创建视觉分隔。以下列表包含我们编写的以实现我们设计的规则。

列表 4.6 样式化出版日期

time {
  font: 700 1.5rem 'Oswald', sans-serif;    ①
  text-align: center;                       ①
  text-transform: uppercase;                ①

  border-top: 3px solid #333333;            ②
  border-bottom: 3px solid #333333;         ②
  padding: 12px 0;                          ②

  display: block;                           ③
}
time sup {                                  ④
  font-size: .875rem;
  font-weight: normal;
}

① 字体排印

② 处理边框和填充

③ 使元素表现得像块级元素

④ 样式化“th”

4.1.6 引用

我们想要突出的最后一段文本是文章中第二段之后的<blockquote>。继续我们的主题,就像我们想要突出的所有其他元素一样,我们将使字体更大、更粗。我们还将调整行高并为元素添加边距。将元素与其周围的内容隔离开来使其更容易被发现。通过添加顶部和底部边距,我们在引用和其上下的段落之间添加空间,在元素周围创建空白。通过添加左右边距,我们改变其对齐方式,实际上使其缩进。添加的空白创建了隔离。

让我们也将引号添加到我们的 <blockquote> 中。为了在引语的开头和结尾添加引号,我们可以简单地进入 HTML 并手动添加它们,或者我们可以使用 CSS 逐行编程来完成这项工作。

quotes 属性允许我们定义自定义引号。我们可以将我们想要用作双引号和单引号图标的符号传递给此属性。并非所有语言都使用相同的符号。例如,美式英语使用 “ ... ” 和 ‘ ... ’,但法语使用 « ... » 和 ‹ ... ›。使用 quotes 属性,我们可以自定义我们想要使用的符号。如果我们没有为 quotes 提供值,浏览器默认行为是使用文档上设置的语言的常规符号。

然而,quotes 属性仅定义了符号;它不会添加它们。为了添加它们,我们使用 content 属性的 open-quoteclose-quote 值,结合 ::before::after 伪元素,如列表 4.7 所示。伪元素允许我们通过 content 属性在应用它们的元素之前和之后插入内容。

列表 4.7 样式化 blockquote

blockquote {
  font: 1.8rem/1.25 'Oswald', sans-serif;
  margin: 1.5rem 2rem;
}
blockquote::before { content: open-quote; }
blockquote::after { content: close-quote; }

open-quoteclose-quote 关键字代表由 quotes 属性定义的开头和结尾引号。因为我们没有在我们的 blockquote 规则中添加 quotes 声明,所以浏览器将使用文档语言的常规符号,我们在 <html> 标签的 lang 属性中将其设置为 en-USen-US 的值指定我们的文档是用美式英语编写的;因此,浏览器渲染的符号是 “ 和 ”,正如我们在图 4.9 中看到的那样。

图 4.9 样式化的标题、标题、副标题和引语

在我们的引语样式化之后,让我们将注意力转向文章中间的列表。

4.2 使用 CSS 计数器

我们的文章包含一个无序列表(项目符号列表)。目前,每个列表项前面都有一个默认的项目符号。我们可以通过使用 list-style-type 属性来改变我们的项目符号的外观。默认情况下,我们可以选择圆点(•)、圆圈(○)、方块(▪),以及多种语言、字母表和数字格式中的数字或字母。但假设我们想要我们的项目符号是一个表情符号——具体来说,是热饮表情符号(☕)。我们必须创建一个自定义列表样式。

要创建我们的自定义列表样式,我们将使用 @counter-style at 规则。我们在第三章创建关键帧时使用了 at 规则。在这种情况下,我们不是定义动画将如何行为,而是定义列表的外观和行为。这个 at 规则被称为 counter-style,因为它专门针对 CSS 中列表项的内置计数机制。在底层,无论列表是有序的还是无序的,浏览器都会跟踪列表中项目的位置——也就是说,它会计数项目。

与关键帧(我们将其命名为以便在 animation 属性内部引用)一样,我们将命名我们的 @counter-style,以便我们可以使用 list-style 属性引用它并将其应用到我们的列表。让我们将我们的列表样式命名为 emoji。因此,我们的 at 规则将是 @counter-style emoji { }

接下来,我们将在我们的 at 规则内部定义 list-style 需要的行为。我们将使用三个属性:symbolssystemsuffix

4.2.1 符号描述符

symbols 描述符定义了将用于创建项目符号样式的元素。为了将我们的表情符号作为要使用的符号,我们可以直接使用表情符号或使用其 Unicode 值。

Unicode 是一种字符编码标准,它指定了如何将 16 位二进制值表示为字符串。换句话说,它是我们表情符号的代码表示。实际的表情符号图像由操作系统和浏览器决定,这就是为什么我们在 iOS 和 Android 等设备上看到表情符号的外观有所不同。Unicode 值告诉机器如何渲染。

我们使用类似 mng.bz/GRQJ 的查找表来找到我们表情符号的此值。☕ 被列为以下代码:U+2615。为了告诉我们的 CSS 我们正在使用 Unicode 值,我们将用反斜杠 (\) 替换 U+。使用 Unicode 值,我们的声明值将是 symbols: "\2615"。如果我们使用表情符号,我们的声明值将是 symbols: ☕;。

接下来,我们需要定义我们的 system 描述符。

4.2.2 系统描述符

无论列表类型(有序或无序列表),浏览器在底层都会根据列表项在列表中的位置跟踪它所设置的样式。第一个项的整数值是 1,第二个是 2,依此类推。system 描述符值定义了将此整数值转换为我们在屏幕上看到的视觉表示所使用的算法。

我们将使用 cyclic 值。之前,我们在 symbols 声明中只提供了一个表情符号,但我们可以使用空格分隔的列表包含多个不同的表情符号。cyclic 值告诉浏览器循环遍历这些值,当用完时,从列表开头重新开始。因为我们只有一个值,浏览器将把 ☕ 应用到第一个列表项,然后符号用完。在第二个列表项之前用完符号后,浏览器从列表开头重新开始,这次将 ☕ 应用到第二个列表项。然后浏览器再次运行,移动到第三个列表项,循环继续。最后,我们将设置一个后缀。

4.2.3 后缀描述符

suffix 描述符定义了项目符号(我们的表情符号)和列表项内容之间的内容——默认情况下是一个句点。我们想要在表情符号和列表项内容之间替换句点,用空格。因此,我们将我们的 suffix 描述符值设置为 " "(一个空格)。

4.2.4 将一切组合起来

我们的 counter-style 定义完成后,可以将其应用到我们的列表中。记住,我们给 counter-style 规则命名为 emoji。我们将使用这个名称作为列表的 list-style 属性值,如下所示。

列表 4.8 样式化列表

@counter-style emoji {     ①
  symbols: "\2615";        ②
  system: cyclic;
  suffix: " ";
}

article ul {
  list-style: emoji;       ③
}

① 定义自定义列表样式的 at 规则

② ☕

③ 将自定义列表样式应用于文章的列表

图 4.10 显示了我们的新样式列表。

图 4.10 使用 ☕ 作为计数器的样式化列表

4.2.5 @counter 与 list-style-image

另一种更改正在使用的列表项标记的方法是使用 list-style-image 属性并将其分配给一个图像,类似于我们可以通过使用 background-image 属性来设置背景图像的方式。我们没有在这个项目中使用这种方法,因为我们使用了表情符号,这是一个 Unicode 字符而不是图像。计数器还为我们提供了更多的控制,例如分配后缀或指定计数器如何循环显示的项标记。

如果我们只想将标记更改为特定的图片,list-style-image 是完美的。但如果我们想要更精细的控制,或者在我们这个例子中,使用文本,我们需要使用 @counter。让我们继续向下滚动页面,接下来对图片进行样式设置。

4.3 图片样式

从历史上看,报纸是以黑白印刷的。当我们考虑印刷的历史时,新闻纸中的彩色油墨是一个相对较新的东西。因此,为了给我们的设计增添一点复古感,我们将使我们的图像变为灰度。首先,我们将看看如何使用滤镜来改变我们的图像。与印刷不同,在网络上我们需要担心资源加载失败或链接损坏,因此我们还将看看如果图像加载失败,如何使图像优雅地失败。最后,我们将添加一个标题来伴随图像。

4.3.1 使用 filter 属性

就像在照片编辑器或像 Instagram 这样的社交媒体网站上一样,我们可以使用 CSS 对图像应用滤镜。我们可以改变颜色、模糊并添加阴影,例如。图 4.11 显示了我们可以通过在 CSS 中使用滤镜对图像进行的一些操作示例。查看 CodePen 中的此代码示例以查看其效果:codepen.io/michaelgearon/pen/porovxJ

图 4.11 使用 filter 属性修改的图片示例

如果我们考虑数字时代之前的摄影,当我们使用胶片并需要去商店冲洗时,我们通过在镜头上添加一个半透明的圆盘来应用滤镜,这改变了进入相机箱和胶片的光线。通过改变光线的性质,我们改变了产生的图像。例如,如果我们拍照时使用红色滤镜,那么只有红色波长的光线被允许通过;我们的照片被染成了红色。偏光太阳镜是另一种改变通过镜头进入的光线的滤镜的例子。

我们仍然可以在数码相机上使用物理过滤器。然而,在许多情况下,过滤器是在拍照后以数字方式应用的。

在 CSS 中,我们使用filter属性对图像应用过滤器;然后我们使用一个函数来定义过滤器应有的行为。您可以在mng.bz/zmYA找到可用函数的列表。我们将使用grayscale()函数使我们的图片看起来像黑白照片。

grayscale()函数接受一个百分比,表示我们想要减少图像中颜色的程度。我们想要移除所有颜色,所以我们将传递一个100%的值。因此,我们的规则将是img { filter: grayscale(100%) }。图 4.12 显示了应用在我们图像上的过滤器。

图 4.12 灰度图像

在使用过滤器之前,需要考虑它们对网站性能的影响。一些过滤器函数,如grayscale(),对浏览器来说处理起来相对简单,但像drop-shadow()blur()这样的函数可能会消耗大量资源。如果我们发现我们在大量图像上应用了许多过滤器,我们应该考虑过滤器对整体页面性能的影响,以及我们是否应该预处理图像而不是用 CSS 应用更改。

4.3.2 处理损坏的图像

即使是最彻底的勤勉和最佳测试实践,损坏的图像链接也可能发生。让我们添加一些回退机制,以确保如果我们的图像无法加载(无论原因如何),我们仍然能为用户提供积极的体验。

首先,让我们故意破坏我们的链接。在 HTML 中,我们将图像的路径替换为项目中不存在的图像文件,如下所示:<img src="./my-broken-image.jpg" alt="my broken link" />。图像将显示为损坏,如图 4.13 所示。

图 4.13 带有alt文本的损坏链接

注意到在alt属性中提供的文本会被显示出来。alt属性允许辅助技术向用户告知正在显示的图像。一个常见的用例是盲人用户通过屏幕阅读器访问内容。在这种情况下,因为图像损坏,文本替换了图像。尽管这种情况并不理想,但在图像损坏的情况下,用户仍然可以了解图像原本应该提供的内容。

在我们的情况下,图像纯粹是装饰性的,不提供任何内容价值,因此如果链接损坏,我们将隐藏图像。那里将什么都没有,但“什么都没有”比破损图像图标更不显眼。由于 CSS 中无法检测图像是否损坏,我们需要使用一点 JavaScript 来知道何时隐藏图像。我们将使用 onerror JavaScript 事件处理器来触发以下样式更改:<img src="..." alt="..." onerror="this.style.display='none'" >。这里对我们有意义的代码片段是 onerror 属性。当发生错误时,onerror 属性内的 JavaScript 触发并将图像的 display 属性设置为 none,隐藏图像。我们可以看到,在图 4.14 中,我们的破损图像缺失。

图 4.14 破损的图像缺失。

onerror 代码仅在图像加载失败时触发,因此让我们修复我们的资源路径到我们的图像,但保留错误处理:<img src="./image.jpg" alt="" onerror="this.style.display='none'" >。现在我们的图像已恢复(图 4.15),但我们有一个安全措施以防它失败。

图 4.15 恢复的图像及备用

接下来,让我们给图像添加一个标题。

4.3.3 格式化标题

图像没有标题,因此我们将通过使用 <figure><figcaption> HTML 元素来添加一个标题。然后我们将对其进行样式化。

这两个元素是相辅相成的。<figure> 包含图像,然后是可选的 <figcaption>。在书籍和其他出版材料中,图表、图表或图像下面通常有文本来描述它或将其与文本相关联。从语义上讲,将图像和标题分组的好处是程序性地将图像与其标题链接起来。从样式化的角度来看,将元素与其标题一起放在父元素中允许我们将元素及其标题作为一个单元进行定位。以下列表显示了如何更改 HTML 以添加图像和标题。

列表 4.9 向 HTML 添加 <figure><figcaption>

<figure>                                                                  ①
  <img src="./image.jpg" alt="" onerror="this.style.display='none'" />    ②
  <figcaption>Golden Gate Bridge</figcaption>                             ③
</figure>                                                                 ④

① 图像开始

② 我们的图像

③ 我们的图像标题

④ 图像结束

让我们开始样式化图像和标题,首先移除当前应用于图像的浏览器提供的边距(图 4.16)。

图 4.16 带有浏览器提供样式的 <figure>

接下来,我们将恢复底部边距,以便我们的标题与下面的段落保持分离。最后,我们将居中图像和标题。我们将标题文本的样式设置为使用 Oswald 字体家族(我们用于所有标题的字体)来在视觉上将其与文章文本区分开来。以下列表显示了用于样式化图像和标题的 CSS。

列表 4.10 figurefigcaption 样式

figure {
  margin: 0 0 12px 0;      ①
  text-align: center;
}
figcaption {
  font-family: 'Oswald', sans-serif;
}

① 内边距简写属性:上、左和右边距设置为 0,下边距设置为 12px

图 4.17 展示了到目前为止我们在项目上取得的进展。到目前为止,页面在窄屏幕上看起来不错,但我们仍然需要在宽屏幕上显示我们的列。接下来,我们将探讨如何使用 CSS 多列布局模块创建多列布局。

图 4.17 展示了到目前为止的进展,包括样式化的图表和图像标题

4.4 使用 CSS 多列布局模块

CSS 多列布局模块可能不如 Grid 和 Flexbox 那样广为人知,但它的用途同样重要。此模块的目的是允许内容在多个列之间自然流动。它的工作方式类似于我们在 Microsoft Word 或 Google Docs 文档中创建多列布局的方式。我们将列分配给内容的一部分,内容自然地从一列流到另一列。因为我们希望我们的内容仅在较宽的屏幕上以列的形式显示,所以我们将使用媒体查询在窗口达到特定大小时有条件地应用我们的列。

4.4.1 创建媒体查询

媒体查询是一种 at 规则;我们在第二章中简要介绍了它,当时我们更改了网格布局,使其依赖于屏幕宽度。就像我们本章早些时候使用的 @counter-style 一样,它以 at (@) 符号开头,后跟标识符 media。然后我们设置媒体查询内规则应用时的指令。我们希望在窗口宽度大于或等于 955 像素时放置内容。因此,我们的媒体查询将是 @media(min-width: 955px) {}。图 4.18 分解了查询的各个部分。在媒体查询内部,我们将定义我们的列。

图 4.18 媒体查询分解

4.4.2 定义和样式化列

我们有两种定义列创建方式的方法:

  • 指定列宽。浏览器将在可用空间内创建尽可能多的该宽度的列。

  • 指定我们想要多少列。浏览器将在可用空间内适应该数量的等宽列。

我们将选择第二个选项,因为我们已经知道我们想要创建三列。我们特别针对文章,并使用 column-count 属性将数量设置为 3,如下面的列表所示。

列表 4.11 根据屏幕宽度有条件地将文章分为三列

@media(min-width: 955px) {    ①
  article {
    column-count: 3;          ②
  }
}

① 媒体查询

② 设置我们想要多少列

图 4.19 展示了使用列表 4.11 中的 CSS 将文章布局为三列。

图 4.19 三列布局

接下来,我们将调整列之间的间距,并在它们之间添加垂直线。让我们从垂直线开始。

4.4.3 使用 column-rule 属性

为了在我们的列之间创建清晰的分隔,我们将使用column-rule属性添加一条垂直线。与边框和外框一样,我们需要设置线型、宽度和颜色。为了保持我们的线条工作一致,我们将使用与页面顶部日期上方和下方边框相同的颜色和样式。然而,我们将使线条略窄一些。

屏幕顶部的线条将内容类型(标题、日期和文章)分开。在这里,我们处于相同的内容类型中。我们添加线条是为了使列之间的视觉分隔更容易;我们不希望打断内容。我们希望线条不那么突出,所以我们将它们做得更细。

为了创建线条,我们在媒体查询中现有的文章规则内添加了column-rule: 2px solid #333333;。现在我们的文章看起来像图 4.20。

图片

图 4.20 添加了垂直线的列

在线条到位后,我们看到文章本身和日期之间有一些拥挤,我们可以在我们的线条和文本之间使用更多空间。

4.4.4 使用 column-gap 属性调整间距

现在我们需要做两件事:增加文章日期和文章正文之间的容器间距,以及增加文章内部列之间的间隔。为了调整文章和日期之间的间距,我们将在文章顶部添加36px的边距。因为确定使用值并不是一门绝对的科学,有时我们需要进行一些试错来确定页面上看起来合适的内容。我们希望留出足够的空间,使每个项目都有其自己的空间并且清晰可见,但又不至于空间过大,使得项目看起来过于分散。

格式塔设计原则

设计的格式塔原则是一组描述人类感知原则的集合,它描述了人类如何将相似元素分组。七个原则之一是邻近性,它讨论了靠近在一起的事物看起来比间隔更远的事物更相关。有关格式塔原则的更多信息,请参阅mng.bz/0yNv

在处理完文章和日期之间的空间后,让我们将注意力转向列之间的空间。为了在垂直线和文本之间添加间隔,我们将使用column-gap属性,该属性定义了我们希望在列之间拥有的空白量。我们将我们的设置为42px;

我们继续在媒体查询中添加这些样式,如列表 4.12 所示,因为我们希望它们只在我们的布局为列时应用。我们不希望这些样式更改应用于较窄的屏幕。

列表 4.12 更新的媒体查询和文章规则

@media (min-width: 955px) {
  article {
    column-count: 3;
    column-rule: 2px solid #333333;
    column-gap: 42px;
    margin-top: 36px;
  }
}

在做出这些调整(图 4.21)后,让我们将注意力转向引用。

图片

图 4.21 调整间距后的布局

在本章的早期,我们设计了块引用的样式,使其突出。但现在我们有了多列格式,它在页面上的其他视觉元素中有点迷失。让我们让它跨越多个列以使其更加突出。

4.4.5 使内容跨越多个列

我们可以通过使用 column-span 属性来使元素跨越多个列。我们的选择是 allnone。因为我们想让引用跨越整个页面,所以我们将选择 all。在我们的媒体查询中,我们将添加以下规则:blockquote { column-span: all }。这个规则导致图 4.22 所示的布局。

图片

图 4.22 由于跨越列的 blockquote 而导致的内容重新排列

注意到内容流程已经改变。我们添加了箭头以显示通过使引用跨越屏幕而引入的新流程。我们不再将整个文章从左上角到右下角均匀分布在整个列中,而是将 column-span: 设置为 all 应用于引用,因此引用之前的内容现在从左上角到右上角跨越页面上的引用上方。引用之后的内容也做同样处理。由于内容跨越,我们改变了文本通过列的流程。

当我们查看内容流程时,我们注意到标题和图片已经被分割到两个列中,这并不理想。让我们防止这种情况发生。

4.4.6 控制内容断行

为了防止图片及其标题出现在不同的列中,我们可以使用 break-inside 属性与关键字值 avoid,我们将它设置在 <figure> 元素上。通过这个声明,我们通知浏览器在生成列时,元素的内容应作为一个单元保持在一起,而不是被分割到多个列中。换句话说,图片和图题应该保持在一起。我们添加到媒体查询的规则是 figure { break-inside: avoid }。图 4.23 显示了结果输出。

图片

图 4.23 保持图片和标题在一起

4.5 添加最终细节

在我们的内容按我们想要的流程在列中流动后,让我们完善一些最终细节。报纸布局的一个特点是文本经常是齐行的。

4.5.1 文本齐行和连字符

齐行指的是文本主体内行的对齐,如图 4.24 所示。当文本齐行时,文本行从同一位置开始和结束,形成一个框。相比之下,左对齐的文本有参差不齐的结尾。

图片

图 4.24 文本齐行

让我们使段落文本居中。为此,我们将使用 text-align 属性并将其值设置为 justify。为了使行长度相等,我们将额外空间分配到行上。我们可以通过使用 text-justify 属性来调整空间重新分配的方式。如果我们不设置 text-justify 值,浏览器将选择它认为最适合该情况的方式。我们有一个流体设计;它随着窗口大小的变化而增长和缩小。最好的选择可能因窗口大小而异,所以我们将让浏览器决定什么最适合。

然而,我们将添加一些连字符。默认情况下,浏览器不会在行尾断开单词;它们简单地继续到下一行。我们可以通过将 hyphens 属性设置为 auto 来改变这种行为。允许浏览器在行尾断开单词将有助于减少我们之间所需的空白量,以便对齐文本。

列表 4.13 展示了我们的段落规则。我们继续在我们的媒体查询中包含我们的更新,因为这些更改仅在切换到列布局时相关。

列表 4.13 居中段落文本

@media (min-width: 955px) {
  ...
  p {
    text-align: justify;
    hyphens: auto;
  }
}

现在我们的段落看起来像图 4.25 中的那些。

图 4.25 居中和带连字符的段落文本

当我们查看布局时,我们注意到第二列底部的图像看起来有点奇怪,位置不合适。让我们修复这个问题。

4.5.2 将文本围绕图像包裹

为了将图像与后续文本重新连接,我们将图像及其标题推到左边,并让文本围绕图像。为了创建这种效果,我们将使用 float 属性。将 float 属性应用于一个元素将其推到左边或右边,允许文本和内联元素围绕它。

在这种情况下,将图像和标题作为一个单元放在 <figure> 元素内对于样式化很有用。因为这两个项目都包含在 <figure> 中,我们将对图形应用 float,整洁地围绕图像和标题包裹文本。

列表 4.14 展示了如何浮动图形。注意我们给图形添加了一个右边距。因为我们把图形浮动到左边,它把自己放在列的左边,允许文本在剩余的右边空间中围绕它,如图 4.26 所示。右边距在图像和文本之间创建了一个空间,这样文本就不会紧挨着图像的边缘。

列表 4.14 浮动图形

@media (min-width: 955px) {
  ...
  figure {
    float: left;
    margin-right: 24px;
  }
}

图 4.26 浮动图像

正如你在第七章中看到的,我们可以用浮动图像做更多酷的事情。不过,现在让我们专注于我们的报纸页面。我们将要解决的最后一件事情是如何处理在极其宽的窗口中页面的行为。

4.5.3 使用 max-width 和自动边距值

图 4.26 显示,当窗口变得极其宽时,我们的布局开始退化。窗口越宽,问题越严重。越来越多的用户拥有超宽屏幕,因此我们需要考虑如果他们最大化窗口,占据整个屏幕会发生什么。为了处理这种情况,我们将使用与第二章中使用的加载器相同的技巧。我们将为我们的布局设置一个最大宽度,然后将其左右边距设置为auto,这样当窗口宽度大于我们的最大宽度时,容器将在水平方向上居中。

对于我们的页面,我们的容器是body,因此我们将给我们的body一个max-width1200px,并将左右边距设置为auto。我们还需要将background-color从在body上设置移动到在html元素规则上设置;否则,当我们的屏幕宽度超过 1,200 像素时,我们会在页面的左右两侧出现白色带状区域。

这些更改不会进入媒体查询内部。我们将编辑本章开头设置的body样式,并添加一个html规则来设置背景颜色。以下列表显示了我们的更改。

列表 4.15 bodyhtml元素的更改

html { background-color: #f9f7f1 }

body {
  background-color: #f9f7f1;       ①
  font-family: 'PT Serif', serif;
  color: #404040;
  padding: 0 24px;
  max-width: 1200px;               ②
  margin: 0 auto;                  ③
}

① 将背景颜色从 body 规则移动到 html 规则

② 设置页面可以成为的最大宽度

③ 使页面居中

经过这些最终更改,我们有一个适用于移动和桌面用户的页面。图 4.27 显示了我们的最终布局。

图片

图 4.27 完成布局

摘要

  • 主题是在整个应用程序中保持的一般外观和感觉。

  • 我们可能需要导入我们的字体,因为很少的字体是普遍可用的。由于没有官方定义的 Web 安全字体列表,我们应该始终使用关键字回退。

  • 创建视觉层次结构将帮助我们的用户在页面上定位自己并识别重要信息。

  • 我们可以控制浏览器在指令显示引号时使用的符号。

  • 我们可以通过使用counter-style at 规则来自定义列表显示其项目符号的方式。

  • 过滤器允许我们改变图像的外观。

  • 我们可以通过使用 CSS 多列布局模块来创建多列布局。

  • 在创建多列布局时,我们可以使内容跨越所有列。

  • 我们可以使浏览器在行尾使用连字符来断开单词。

  • 浮动允许我们围绕元素包裹文本。

5 张具有悬停交互的总结卡片

本章节涵盖

  • 使用background-clip属性裁剪静态背景图像

  • 使用过渡效果在悬停时显示内容

  • 使用媒体查询根据设备能力和窗口大小选择样式

总结卡片用于多种目的,无论是显示电影的预览、购买房产、预览新闻文章,还是(在本章中)显示酒店列表。通常,总结卡片包含标题、描述和行动号召;有时,它还包含一个图像。图 5.1 显示了我们将在这个项目中创建的卡片。

图片

图 5.1 成品

卡片将排成一行,使用 CSS Grid 布局模块进行布局。每个卡片都将有自己的背景图像,内容放置在顶部。如果用户在支持悬停且屏幕宽度至少为 700 像素的设备上查看卡片,他们将能够看到标题,然后悬停在卡片上,这将揭示简短的描述和一个橙色的行动号召按钮,以与黑色背景形成对比(图 5.2)。

图片

图 5.2 成品上的悬停效果

对于不支持悬停或屏幕宽度小于 700 像素的用户设备,我们将显示所有信息而不使用悬停,以确保用户体验不受影响(图 5.3)。

图片

图 5.3 小型或触摸设备上无法处理悬停状态的成品

本项目的另一部分是页眉,我们希望使其突出并具有视觉吸引力。为此,我们将探索background-clip属性,看看我们如何可以在文本周围裁剪图像。

5.1 开始

列表 5.1 和列表 5.2 包含了我们在本章中将要构建的页面的起始 CSS 和 HTML。要跟随我们为页面添加样式的过程,您可以从 GitHub 仓库mng.bz/KlaO或从 CodePencodepen.io/michaelgearon/pen/vYpaQPO下载起始 HTML 和 CSS。

移动端和桌面端体验将使用相同的 HTML 和样式表。类似于我们在第四章中所做的那样,我们将使用媒体查询根据浏览器大小和能力来调整样式。

列表 5.1 显示了我们的起始 HTML。每个卡片都包裹在一个<section>元素中,并包括其标题(<h2>)、描述(<p>)和行动号召(<a>)。

列表 5.1 起始 HTML

  <header>
    <h1>Hotels</h1>                            ①
  </header>
  <main>
    <section class="flamingo-beech">           ②
      <div>
        <h2>Meeru Island Resort & Spa</h2>     ③
        <p>The stylish Meeru Island ...</p>    ④
        <a href="#">Learn more</a>             ⑤
      </div>
    </section>                                 ⑥
    ...

  </main>

① 页面标题

② 第一张总结卡片的开始

③ 卡片标题

④ 卡片描述(当浏览器允许时仅在悬停时显示)

⑤ 卡片行动号召

⑥ 第一张总结卡片的结束

我们开始的 CSS(列表 5.2)包括一些基本样式来设置我们的页面。对于主体,我们增加了 40 像素的边距,并在所有四边添加了 20 像素的填充。我们使用 Google Fonts——这次是 Cardo 字体家族,常规重量,斜体版本——来描述每张卡片。对于标题,我们将使用 Rubik 字体的常规和粗体重量。这种字体是一个不错的选择,因为它结合了良好的可读性和圆角,提供了一种与 Cardo 字体相得益彰的非正式感。注意,当我们加载多个 Google Fonts 时,我们可以将导入合并为一个请求。

列表 5.2 起始 CSS

@import url("https:/ /fonts.googleapis.com/css?
➥ family=Cardo:400i|Rubik:400,700&display=swap");     ①

body {
  margin-top: 40px;
  padding: 20px;
}

① 请求加载 Cardo 和 Rubik 两种字体

当我们开始为我们的项目添加样式时,我们的页面看起来像图 5.4。

图片

图 5.4 起始点

5.2 使用网格布局页面

一个好的起点是回顾我们卡片和网页的整体布局。我们需要考虑我们布局的三个方面:

  • 标题和主要内容

  • 卡片的容器

  • 卡片内的内容

我们将在所有三个用例中使用 CSS Grid 布局模块进行布局。

注意:CSS Grid 布局模块允许我们在列和行的系统中在垂直和水平轴上放置和定位元素。查看第二章以了解此模块的工作原理。

为了在我们的页面上布局元素,我们将首先为窄屏幕创建样式,并通过使用媒体查询,在构建到更大屏幕尺寸的过程中编辑布局。

5.2.1 使用网格布局

我们的布局由两个地标组成:<header><main>,它们是<body>的直接子元素(列表 5.3)。通过给<body>设置一个值为griddisplay属性,我们将影响<header><main>元素的位置。

列表 5.3 起始 HTML

<body>
  <header>    <!-- title -->
  </header>
   <main>    <!-- cards -->
   </main>
</body>

接下来,我们使用place-items属性来在页面上居中元素。这个属性是合并声明align-itemsjustify-items属性值的简写方式。我们将其值设置为center,使所有项目在其各自的行和列中间对齐。以下列表显示了我们的更新后的body规则。

列表 5.4 定位<header><main>元素

body {
  display: grid;
  place-items: center;
  margin-top: 40px;
  padding: 20px;
}

注意,我们没有定义任何grid-template-rowsgrid-template-columnsgrid-template-areas。默认情况下,当没有声明这些区域时,浏览器会创建一个一列网格,行数与要定位的元素数量相同。在我们的例子中,我们有两个元素:<main><body>。因此,我们的网格有一列和两行(图 5.5)。

图片

图 5.5 一行两列网格

<header><main> 的宽度通过在网格内调整,仅占用它们内容所需的水平空间。因为 <header> 的内容较窄(包含单词 hotel<h1>),页面标题会自动居中在页面上。<main> 元素占据其可用的全部宽度,因为 Flamingo Beach 的描述(在第二张卡片中)需要全部宽度,并且甚至可以包裹。如果我们进一步扩展屏幕宽度,我们会看到 <main> 元素也会居中(图 5.6)。

图片

图 5.6 宽屏幕上的居中main

我们还将依赖网格的默认功能,省略定义行和列,因为我们希望在小屏幕上保持卡片堆叠。为了在卡片之间添加空间,我们包括一个1rem的间隙。我们还限制<main>元素的宽度最大为 1024 像素,以防止我们在宽屏幕上将卡片水平对齐后,卡片在宽屏幕上过于分散(第 5.2.2 节)。以下列表显示了我们的更新 CSS,它保持了卡片的堆叠,但在卡片之间添加了 1rem 的间隙(图 5.7)。

图片

图 5.7 应用到<main>的网格

列表 5.5 在窄屏幕上定位卡片

main {
  display: grid;
  max-width: 1024px;
  grid-gap: 1rem;
}

5.2.2 媒体查询

目前,我们的卡片是垂直堆叠的——这是大多数情况下 HTML 元素的默认行为。这种布局在具有相对较窄屏幕的移动设备上是有意义的。然而,对于桌面屏幕,由于浏览器窗口可以非常宽,我们可以通过使用媒体查询来利用水平空间。我们可以定义一些媒体查询来调整布局:

  • 如果窗口宽度大于或等于 700 像素,我们将调整网格以有两个等宽的列,并将每个部分的宽度设置为正好 350 像素。

  • 在 950 像素时,我们再次调整布局,将布局调整为四个等宽的列,覆盖前面媒体查询中设置的grid-template-columns值。height属性值保持为 350 像素,因为前面媒体查询的条件(min-width: 700px)仍然满足。

如果这些媒体查询的要求都不满足(当浏览器窗口宽度小于 700 像素时),卡片将垂直堆叠在单列中。以下列表显示了创建的两个媒体查询。

列表 5.6 卡片的布局

@media (min-width: 700px) {      ①
  main {
    grid-template-columns: repeat(2, 1fr);
  }
  main > section {
    height: 350px;
  }
}
@media (min-width: 950px) {      ②
  main {
    grid-template-columns: repeat(4, 1fr);
  }
}

① 媒体查询用于确定浏览器窗口是否至少有 700 像素宽。如果是这样,则使用查询内的样式。

② 第二个媒体查询用于确定浏览器窗口是否至少有 950 像素宽。如果是这样,此查询将覆盖前面的查询,并将网格设置为四列宽。

图片

图 5.8 800 像素宽屏幕上的布局

图 5.8 和图 5.9 分别显示了浏览器窗口宽度为 800 像素和 1000 像素时的输出。

图片

图 5.9 1000 像素宽屏幕上的布局

拥有我们的布局后,让我们专注于内容的样式设计,从标题开始。我们将更改 <h1> 元素的字体,并查看如何使用图片来为文本着色。

5.3 使用 background-clip 属性设置标题样式

这个页面的标题“酒店”在视觉上可能更有趣。一种让页面生动起来的方法可以是设置一个漂亮的鲜艳颜色,并将字体家族更新为现代风格。另一种方法是应用背景图像到文本。这些更改可以通过两个实验性属性:background-cliptext-fill-color 来实现。

实验性属性

一些属性的浏览器支持可能是值特定的。background-clip 属性就是其中之一。这个属性在其所有可能值中(除了 text,在撰写本书时,在 Microsoft Edge 和 Google Chrome 中仍然需要供应商前缀)都得到了所有主流浏览器的支持,没有供应商前缀。

实验性属性应谨慎使用,因为它们通常具有非标准实现。有关实验性属性的更多详细信息,请参阅第三章。

通过设置回退颜色值,我们可以减少 background-clip: text 作为实验性属性带来的风险,这样如果这两个属性不起作用,用户将看到没有背景图像的文本。

5.3.1 设置字体

第一步是更新 font-familyweightsize,并将文本转换为 uppercase。以下列表显示了这些更改。

列表 5.7 标题排版

h1 {
  font: 900 120px "Rubik", sans-serif;     ①
  text-transform: uppercase;
}

① 简写字体属性

我们使用了简写的 font 属性。第一个值设置 weight,在这个例子中是 heavy。第二个值是字体大小(120px),然后是我们想要使用的 font-family。如果这个字体无法加载,我们将回退到 sans-serif 字体。

我们通过样式将文本转换为大写,而不是在 HTML 中将所有文本都写成大写字母。使用全部大写字母可能会影响可访问性,因为一些屏幕阅读器可能会将全部大写字母解释为缩写,并逐个读取字母。如果我们通过 CSS 设置文本为大写,我们只是在视觉上样式化文本;字符可以是混合大小写。

此外,我们只有一个页面可以设置样式,这使我们处于独特的位置。在一个传统项目中,我们的样式很可能会应用于多个页面。通过调整我们的样式中的大小写,我们帮助确保整个网站或应用程序的一致性。

还值得注意的是,我们应该少量使用全部大写字母,因为这种格式可能会影响内容的可读性。现在我们的标题看起来像图 5.10。

图片

图 5.10 应用到标题的排版样式

5.3.2 使用 background-clip

现在我们将使用图像来着色字母,本质上是将背景图像应用于字母本身。首先,我们需要在 <h1> 元素上设置一个背景图像。为了确保图像覆盖 <h1> 元素的全部,我们将 background-size 属性的值设置为 cover。此值会自动计算图像所需的宽度和高度,以确保图像覆盖整个元素。

接下来,我们操作图像,使其仅应用于字母,而不是整个 <h1> 元素。这一步是 background-clip 属性发挥作用的地方。此属性基于盒模型定义了背景应该覆盖元素的哪个部分。在我们的例子中,我们将它的值设置为 text,因为我们希望图像显示在字母后面。具有 text 值的此属性仍然需要基于 WebKit 的浏览器(Chrome、Edge 和 Opera)的前缀,因此我们也包括针对这些浏览器的带前缀的属性以实现兼容性。

目前,我们的文本是黑色,阻止了图像的显示。我们必须使字母透明,以便不遮挡我们设置为文本背景的图像。text-fill-color 属性允许我们设置文本的颜色。此属性类似于 color,但如果两个属性都设置了,text-fill-color 将会覆盖 color。因为 text-fill-color 也需要浏览器前缀(对于基于 WebKit 和 Mozilla 的浏览器),所以如果图像没有加载或任何实验性属性失败,我们可以将 color 属性用作回退。

我们使用 text-fill-color 而不是使用值为 transparentcolor 属性,因为我们将会使用 color 创建一个回退方案,以防 background-clip 在用户的浏览器中不起作用。我们将它的值设置为 white,因为我们将在本章后面添加黑色背景。这样,如果 background-clip 失败或不受支持,用户仍然可以看到文本;它将是白色而不是被图像着色。下面的列表显示了我们的更新后的标题类。

列表 5.8 background-clip 文本代码

h1 {
  text-transform: uppercase;
  font: 900 120px "Rubik", sans-serif;
  background: url(background: url("bg-img.jpg");    ①
  background-size: cover;
  -webkit-background-clip: text;                    ②
  background-clip: text;                            ②
  -moz-text-fill-color: transparent;                ③
  -webkit-text-fill-color: transparent;             ③
  color: white;                                     ④
}

① 添加背景图像

② 仅将背景裁剪应用于文本后面

③ 使文本透明,以便图像能够显示出来

④ 回退颜色

当使用前缀时,如果存在非前缀版本,我们会在非前缀版本之前添加 -moz--webkit- 属性。这允许浏览器确保在非实验性版本可用时使用该版本。

在我们的标题样式(图 5.11)完成后,下一个任务是样式化卡片。我们首先关注没有悬停效果的卡片样式,然后创建用于处理支持悬停的宽屏幕卡片的媒体查询。

图 5.11

图 5.11 背景图像裁剪到标题处

5.4 样式化卡片

每个卡片都通过一个具有背景图像的外部 <section> 元素创建,并且有一个内部 <div>,我们将为它设置背景颜色,以保持文本在图像上的可读性。在该 <div> 内是实际的内容。以下列表显示了我们的卡片结构,独立于其他 HTML 部分。

列表 5.9 独立的卡片 HTML

<section class="meeru-island">                 ①
  <div>                                        ②
    <h2>Meeru Island Resort & Spa</h2>         ③
    <p>The stylish Meeru Island Resort...</p>  ③
    <a href="#">Learn more</a>                 ③
  </div>
</section>

① 外部卡片容器。每个部分都有一个基于其描述的酒店的类名。

② 内容容器

③ 内容

为了为卡片的每个部分进行样式设计,我们将从外向内工作,首先为每个卡片的容器进行样式设计,然后是内容容器的样式,最后是内容本身。

5.4.1 外部卡片容器

外部容器是获取背景图像的元素。每个部分为其酒店或度假村获取一个图像。我们将通过其类名单独选择每个部分。然后,我们将为每个部分分配一个背景图像,如下列所示。

列表 5.10 添加背景图像

.meeru-island {
  background-image: url("1.jpg");
}
.flamingo-beech {
  background-image: url("2.jpg");
}
.protur-safari {
  background-image: url("3.jpg");
}
.mountain-view {
  background-image: url("4.jpg");
}

在添加了背景图像(图 5.12)后,让我们配置适用于所有部分的通用样式。

图 5.12 卡片背景图片

我们可以看到图片没有正确居中,并且没有很好地展示酒店和度假村。我们可以通过使用 background-size 属性来调整图片的大小。我们将此属性设置为 cover,以最大化显示图片,同时如果图片的宽高比与卡片的不同,则不会留下任何空白可见。我们还添加了 #3a8491(青绿色)作为后备颜色。最后,我们在卡片上添加了 border-radius 以使角落弯曲并软化边缘。列表 5.11 显示了我们的容器样式。

列表 5.11 卡片容器样式

main > section {
  background-size: cover;
  background-color: #3a8491;
  border-radius: 4px;
}

在处理了外部容器(图 5.13)后,让我们继续到内容容器。

图 5.13 样式化的外部卡片容器

5.4.2 内部容器和内容

目前,我们的文本不可读;深色文本在图像背景上难以阅读,并且靠近外部容器的边缘。为了提高可读性,我们将内部容器的 background-color 设置为 rgba(0, 0, 0, .75),这是一种带有一定透明度的黑色。我们还将文本颜色更改为 whitesmoke 并将其居中。通过在设计中使用纯黑或纯白,我们为整体构图带来了一种更柔和的感觉。

在添加背景颜色后,我们在内容容器中添加了 1rem 的填充,以使文本远离深色背景的边缘,并添加了 1rem 的边距,以在图片边缘和背景开始处留出空间。最后,我们调整了卡片内文本的 font-sizefont-weightline-heightfont-family。以下列表显示了 CSS 代码。

列表 5.12 卡片内容样式

main > section > div {                       ①
  background-color: rgba(0, 0, 0, .75);      ①
  margin: 1rem;                              ①
  padding: 1rem;                             ①
  color: whitesmoke;                         ①
  text-align: center;                        ①
  font: 14px "Rubik", sans-serif;            ①
}                                            ①

section h2 {                                 ②
  font-size: 1.3rem;                         ②
  font-weight: bold;                         ②
  line-height: 1.2;                          ②
}                                            ②

section p {                                  ③
  font: italic 1.125rem "Cardo", cursive;    ③
  line-height: 1.35;                         ③
}                                            ③

① 卡片内容容器

② 卡片标题

③ 卡片内容

在应用了样式(图 5.14)后,最后需要样式的内容部分是我们的链接。

图 5.14 卡片内部容器和排版

因为我们的链接充当行动号召,引导用户查看更多关于酒店或度假村的信息,所以我们希望使其加粗并引人注目(列表 5.13)。为了达到这个目的,因为卡片内部的大部分元素颜色都比较深,我们将给链接一个明亮的黄橙色(#ffa600)背景,并将其文字颜色改为接近黑色。我们还会添加填充。但由于链接默认是一个内联元素,我们希望将其 display 属性的值更改为 inline-block,这样填充就会影响元素的高度。

列表 5.13 链接样式

a {
  background-color: #ffa600;
  color: rgba(0, 0, 0, .75);
  padding: 0.75rem 1.5rem;
  display: inline-block;
  border-radius: 4px;
  text-decoration: none;
}

a:hover {
  background-color: #e69500;
}

a:focus {
  outline: 1px dashed #e69500;
  outline-offset: 3px;
}

为了与卡片匹配,我们将链接的 border-radius 设置为 4px,并最终处理 hoverfocus 状态。我们不会使用下划线,我们将移除它,在 hover 状态下,我们将稍微加深背景颜色,在 focus 状态下,我们将添加一个偏移 3 像素的虚线轮廓。图 5.15 显示了我们的样式化链接。

图 5.15 样式化链接

如果所有的链接都没有水平对齐,看起来会有些奇怪,并且似乎没有组织。为了使所有链接对齐,我们将再次使用 grid。我们将给内部容器一个 display 值为 grid,并将 grid-template-rows 值设置为 min-content auto min-content,同时将内部容器的高度设置为 100% 减去我们为其分配的填充和边距(图 5.16)。

图 5.16 水平对齐卡片元素

在本章的早期部分,我们给内部容器设置了 margin1rempadding1rem,这意味着它需要占据的空间高度等于 100% 减去 4rem(1 rem 的填充和 1 rem 的边距在顶部和底部,总共等于 4 rem)。为了在 CSS 中实现这种效果,我们使用 calc() 函数来为我们进行计算,将 calc(100% - 4rem) 分配给 height 属性。定义的行(grid-template-rows: min-content auto min-content)和设置的高度组合创建了一个布局,其中标题和链接只占据它们所需的空间,中间部分(段落元素)则得到剩余的空间。

最后,为了在卡片中间垂直居中段落内容,我们使用 align-items 属性并设置为 center,同时移除浏览器自动添加到 <h2> 的底部边距。如果我们保留标题底部的边距,那么段落顶部的空间会比底部多,因为 min-content 会考虑元素上的边距。因为卡片底部的链接没有边距,所以与段落下方相比,段落上方的空白空间会不成比例。下面的列表显示了我们的布局调整。

列表 5.14 内部容器布局调整

main > section > div {
  background-color: rgba(0, 0, 0, .75);
  margin: 1rem;
  padding: 1rem;
  color: whitesmoke;
  text-align: center;
  height: calc(100% - 4rem);
  display: grid;
  grid-template-rows: min-content auto min-content;
  align-items: center;
}

section h2 {
  font-size: 1.3rem;
  font-weight: bold;
  line-height: 1.2;
  margin-bottom: 0;
}

最后的调整完成了卡片布局(图 5.17)。接下来,我们将关注在宽度足够大(宽度大于或等于 700 像素)且具有悬停功能的设备上显示和隐藏内容部分。

图片

图 5.17 样式化卡片

5.5 使用过渡动画在悬停和焦点内动画化内容

首先,我们需要创建一个媒体查询,检查设备是否支持hover交互,浏览器窗口是否至少 700 像素宽,以及用户是否在他们的机器上启用了prefers-reduced-motion

减少运动偏好

一些用户希望退出运动密集的动画。他们可以通过在设备上启用一个设置来实现,该设置通过prefers-reduced-motion属性传达给浏览器。我们想确保我们尊重用户的设置。因此,我们将声明该设置未设置(具有no-preference的值)作为我们确定是否动画化内容的查询的一部分。有关prefers-reduced-motion的更多信息,请参阅第三章。

我们的媒体查询是@media (hover: hover) and (min-width: 700px) and (prefers-reduced-motion: no-preference) { }。请注意,我们可以链式多个参数,只有当这些参数都满足时,查询中的 CSS 才会应用。

要隐藏除标题之外的所有内容,我们将使用transform属性和translateY()值将内容向下移动到卡片的底部。translateY()值允许我们将内容垂直移动到页面流之外;被移动元素周围的内容不受移动影响,不会重新定位或让路。

为了计算元素需要移动的距离,我们再次使用calc()函数。我们将通过以下列表所示,将标题向下移动卡片的高度(350px)减去8rem(容器的顶部边距 + 容器的顶部填充 + 标题的大小)。

列表 5.15 隐藏非标题内容

@media (hover: hover) and (min-width: 700px) and
➥ (prefers-reduced-motion: no-preference) {
  main > section > div {
    transform: translateY(calc(350px - 8rem));
  }
}

卡片的内部部分向下移动,如图 5.18 所示。

图片

图 5.18 将内容向下移动

因为我们要在用户停止悬停在部分上方时动画显示内容,所以我们不想让底部的尾随内容保持:如果用户悬停在图片外溢的内容上,内容将向上移动到图片中,失去悬停状态,然后再次向下移动。这种行为会重复,产生闪烁。因此,我们将为我们的内部容器设置5rem的高度,并在段落和链接隐藏时隐藏溢出。

注意,在第二个卡片中,当内容应该被隐藏时,段落内容的一小部分仍然可见,因此我们还将使用不透明度隐藏非标题内容。此外,我们还将使用translateY()将内容向下移动1rem,这样当我们在hover时将其动画回时,它将获得一点运动效果。

所有的 CSS,用于隐藏内容和缩短内部容器,如下所示。为了选择所有不是标题的内容,我们可以使用:not()伪类。

列表 5.16 隐藏非标题内容

@media (hover: hover) and (min-width: 700px) and (prefers-reduced-motion:
➥ no-preference) {                              ①
  main > section > div {                         ②
    transform: translateY(calc(350px - 8rem));   ②
    height: 5rem;                                ②
    overflow: hidden;                            ②
  }                                              ②
  main > section > div > *:not(h2) {             ③
    opacity: 0;                                  ③
    transform: translateY(1rem);                 ③
  }                                              ③
}

① 媒体查询

② 移动并缩短内部内容容器

③ 隐藏所有非<h2>内容

not()伪类允许我们过滤选择器。在这种情况下,我们想要定位任何不是<h2>的东西。图 5.19 展示了这个过程。

图 5.19 选择内部容器内所有不是<h2>的内容

现在内容已被隐藏(图 5.20),我们可以专注于再次显示它。

图 5.20 隐藏内容

要再次显示内容,我们需要撤销我们在hoverfocus状态下所做的所有隐藏操作。因为我们没有从文档对象模型(DOM)中移除链接,它们只是视觉上被隐藏了;在程序上,它们仍然存在,用户可以通过键盘切换到链接。因此,当用户悬停在卡片上或链接获得焦点时,我们都需要显示内容。因为我们希望在子元素(链接)获得焦点时对祖先元素(内容容器)进行操作,我们可以使用:focus-within伪类。这个伪类允许我们根据元素是否有后代当前处于焦点状态有条件地应用样式。

因此,当链接获得焦点或章节被悬停时,我们通过将translateY()参数设置为0(无垂直位移)并将内部容器的高度设置为350px(外部容器的高度)减去4rem(容器垂直内边距和边界的总和)来将容器恢复到原位。我们还需要恢复段落和链接,它们的透明度被设置为0,并且已经被向下移动了1rem

我们将通过为显示和隐藏的元素添加过渡效果来完成hoverfocus-within效果。因为我们已经预定义了要改变的状态,并且希望动画只运行一次,当变化发生时,我们不需要使用关键帧。我们可以简单地指示 CSS 在变化发生时动画化所有变化,使用transition属性,值为all 700ms ease-in-out。所有变化都将被动画化;动画将花费 700 毫秒完成;动画将以慢速开始,加速,然后在完成前再次减速。以下列表显示了我们的hoverfocus-withinCSS。

列表 5.17 在hoverfocus-within时显示内容

@media (hover: hover) and (min-width: 700px) and
➥ (prefers-reduced-motion: no-preference) {
 main > section > div {
  transform: translateY(calc(350px - 8rem));
  height: 5rem;
  overflow: hidden;
  transition: all 700ms ease-in-out;       ①
 }
 div > *:not(h2) {
  opacity: 0;
  transform: translateY(1rem);
  transition: all 700ms ease-in-out;       ①
 }
 section:hover div,                        ②
 section:focus-within div {                ③
  transform: translateY(0);
  height: calc(350px - 4rem);
 }

 section:hover div > *:not(h2),            ④
 section:focus-within div > *:not(h2){     ⑤
  opacity: 1;
  transform: translateY(0);
 }
}

① 动画变化

② 在章节hover时,将容器恢复到原位

③ 在章节focus-within时,将容器恢复到原位

④ 在hover时,将容器内的所有非<h2>元素恢复到原位,具有全不透明度

⑤ 在章节focus-within时,将容器内的所有非<h2>元素恢复到原位,具有全不透明度

应用这些更改(图 5.21)后,完成项目剩下的工作就是在我们的页面上设置背景。

图片

图 5.21 hoverfocus-within效果

要使图片更加突出,我们将为整个页面添加深灰色、几乎黑色的背景。要应用背景颜色,我们将添加值为#010101background属性到现有的body规则中,如下面的列表所示。

列表 5.18 添加背景

body {
  display: grid;
  place-items: center;
  margin-top: 40px;
  padding: 20px;
  background: #010101;
}

图 5.22、5.23 和 5.24 展示了我们在不同屏幕尺寸下的完成项目。

图片

图 5.22 在宽度为 600 像素的窗口中的项目

图片

图 5.23 在宽度为 850 像素的窗口中的项目

图片

图 5.24 在宽度为 1310 像素的窗口中启用prefers-reduced-motion的项目

摘要

  • 网格可以用于整个布局或布局中的单个元素。

  • text-transform属性可以将文本转换为大写,而不会影响内容的可访问性。

  • 请谨慎使用text-transform: uppercase,不要在大量内容区域使用。

  • background-clip属性值为text时,可以剪裁背景图像以围绕文本。

  • background-clip属性值为text时仍需要前缀,并且该属性在实现过程中可能会发生变化。

  • 我们可以使用媒体查询来检查设备是否支持hover,并调整我们的布局,以防止用户在设备不支持hover时看到内容。

  • 我们可以通过使用and来在同一个媒体查询中链式多个条件。

  • 我们可以在媒体查询中使用prefers-reduced-motion来尊重用户对动画和运动的偏好。

  • :not()伪类代表不匹配一系列选择器的元素。

  • translateY()将内容垂直移动,而不会影响重新流。

  • 我们可以使用transition属性在状态之间动画化样式变化。

  • 要根据元素的子代是否处于焦点来有条件地应用样式,我们使用focus-within伪类。

6 创建个人资料卡片

本章涵盖

  • 使用 CSS 自定义属性

  • 使用radial-gradient创建背景

  • 设置图像大小

  • 使用 flexbox 定位元素

在本章中,我们将创建一个个人资料卡片。在网页设计中,卡片是一个包含单个主题信息的视觉元素。我们将应用这个概念来展示某人的个人资料信息,本质上创建了一张数字名片。这种布局通常用于社交媒体和博客网站上,以向读者概述作者信息。有时它包含链接到详细个人资料页面或与个人资料所属的人互动的机会。

为了创建布局,我们将围绕定位做大量工作,特别是使用 CSS Flexbox 布局模块来对齐和居中元素。我们还将探讨如何使矩形图像适应圆形而不扭曲图像。到本章结束时,我们的个人资料卡片将看起来像图 6.1。

图片

图 6.1 最终输出

6.1 开始项目

让我们直接深入看看我们的起始 HTML(列表 6.1),您可以在 GitHub 仓库mng.bz/5197或 CodePencodepen.io/michaelgearon/pen/NWyByWN上找到。我们有一个带有card类的<div>,其中包含在个人资料卡片中展示的所有元素。为了设置博客文章信息,我们将使用描述列表。我们的技术(CSS、HTML 等)以列表形式展示。

描述列表

一个描述列表包含一系列术语,包括描述术语(dt)和任意数量的描述(dd)。描述列表常用于创建术语表或显示元数据。因为我们正在将术语(帖子、点赞和关注者)与其计数(数量)配对,所以这个项目非常适合使用描述列表。

列表 6.1 项目 HTML

<body>
  <div class="card">                                            ①
    <img class="portrait" src="./img/portrait.jpg" alt="">      ②
    <h1>Annabelle Erickson</h1>                                 ③
    <div class="title">Software Developer</div>                 ④
    <dl>                                                        ⑤
      <div>                                                     ⑤
        <dt>Posts</dt>                                          ⑤
        <dd>856</dd>                                            ⑤
      </div>                                                    ⑤
      <div>                                                     ⑤
        <dt>Likes</dt>                                          ⑤
        <dd>1358</dd>                                           ⑤
      </div>                                                    ⑤
      <div>                                                     ⑤
        <dt>Followers</dt>                                      ⑤
        <dd>1257</dd>                                           ⑤
      </div>                                                    ⑤
    </dl>                                                       ⑤
    <p class="summary">I specialize in UX / UI...</p>           ⑥
    <ul class="technologies">                                   ⑦
      <li>CSS</li>                                              ⑦
      <li>HTML</li>                                             ⑦
      <li>JavaScript</li>                                       ⑦
      <li>Accessibility</li>                                    ⑦
    </ul>                                                       ⑦
    <div class="actions">                                       ⑧
      <button type="button" class="follow">Follow</button>      ⑧
      <a href="#" class="message">Message</a>                   ⑧
    </div>                                                      ⑧
  </div>                                                        ⑨
</body>

① 卡片开始

② 个人头像

③ 个人资料持有者的姓名

④ 个人资料持有者的职位

⑤ 文章信息

⑥ 个人简介/关于

⑦ 技术

⑧ 操作

⑨ 卡片结束

当我们开始为卡片添加样式时,页面看起来就像图 6.2 所示。

6.2 设置 CSS 自定义属性

在我们的布局中,特别是当我们为个人资料图像和图像下方卡片顶部的彩色部分添加样式时,我们需要为几个计算使用图像大小值。在 JavaScript 等语言中,当我们有一个将要多次引用的值时,我们使用自定义属性,有时也称为CSS 变量

要创建一个自定义属性,我们需要在变量名前加上两个连字符(--),紧接着是变量名。我们以与任何其他属性相同的方式将值分配给自定义属性:冒号(:)后跟值。因此,CSS 变量声明看起来像这样:--myVariableName: myValue;

与任何其他声明一样,我们需要在规则内定义我们的变量。对于我们的项目,我们将定义我们的颜色和图像大小,然后在列表 6.2 中声明的 body 规则内声明它们。因为我们定义变量在 body 上,所以 <body> 元素及其任何后代都将能够访问这些变量。

图片

图 6.2 起始点

列表 6.2 定义 CSS 自定义属性

body {
  --primary: #de3c4b;                    ①
  --primary-contrast: white;
  --secondary: #717777;                  ②
  --font: Helvetica, Arial, sans-serif;
  --text-color: #2D3142;                 ③
  --card-background: #ffffff;
  --technologies-background: #ffdadd;
  --page-background: linear-gradient(#4F5D75, #2D3142);
  --imageSize: 200px;

  background: var(--page-background);
  font-family: var(--font);
  color: var(--text-color);
}

① 红色

② 灰色

③ 深蓝灰色

注意:我们的线性渐变将从顶部到底部渐变,从深蓝渐变到更深的蓝。要深入了解线性渐变,请参阅第三章。

注意我们可以为变量分配不同类型的值。我们分配颜色,例如在我们的 --primary 变量中(CSS 自定义属性最常见用途之一),但我们还定义了一个大小(--imageSize)、一个字体家族(--font)和一个渐变(--page-background)。

要引用变量并将其用作声明的一部分,我们使用语法 var(--variableName)。因此,为了设置我们的文本颜色,我们声明 color: var (--text-color);。在我们的背景和字体颜色及家族应用后(图 6.3),我们注意到我们的背景在页面底部重复。

图片

图 6.3 向 <body> 添加背景

6.3 创建全高度背景

一种 线性渐变 是一种图像类型。当我们将图像作为 CSS 中元素的背景时,如果图像小于元素,图像将重复,或 平铺。在这种情况下,我们不想让图像重复。我们有两种方法可以解决这个问题:

  • 我们可以通过使用 background-repeat: no-repeat; 来告诉背景我们不希望它重复。然而,由于我们的 <body> 元素的高度仅与其内容相同,如果窗口比内容高,我们将在页面底部留下一个难看的白色条带——这并不理想。

  • 我们的第二个选项(我们将使用的选项)是使 <html><body> 元素占据整个屏幕的高度,而不是根据内容大小调整。

我们将在列表 6.3 中的规则添加到我们的样式表中。我们将重置边距和填充为 0,因为我们想确保我们在窗口内从边缘到边缘。

列表 6.3 使背景高度满屏

html, body {
  margin: 0;
  padding: 0;
  min-height: 100vh;
}

要设置高度,我们使用 min-height,因为如果内容长度大于窗口高度,我们希望用户能够访问内容,并且我们希望背景在内容后面。通过使用 min-height,我们指示浏览器使 <body><html> 元素至少与窗口高度相同。如果内容迫使元素变高,浏览器将使用内容的高度。

我们为min-height设置的值是100vh。视口高度(vh),是一个基于视口自身高度的单元,是百分比为基础的。所以将100vh的值分配给min-height意味着我们希望元素至少有与视口高度相等的 100%的高度。现在我们已经设置了背景(图 6.4),让我们来设置卡片样式。

图 6.4 全屏渐变背景

6.4 使用 Flexbox 进行卡片样式和居中

让我们从样式化卡片本身开始。我们将给它一个白色背景和阴影,以给我们的布局增加一些深度。注意,我们不是使用背景颜色值,而是使用我们的background变量。

我们还将设置卡片的宽度为75vw。视口宽度(vw)是我们之前使用的视口高度(vh)单位的水平对应物。它也是基于百分比的,所以通过将宽度设置为75vw,我们设置了卡片的宽度为浏览器窗口总宽度的 75%。

接下来,我们将进一步限制卡片的宽度,最大宽度为 500 像素。通过使用widthmax-width属性,我们允许卡片在屏幕尺寸较窄时缩小,但在较大屏幕上不会变得过于宽大和杂乱。最后,我们通过使用border-radius来使卡片的角落弯曲,以软化设计。以下列表显示了我们的卡片规则。

列表 6.4 样式化卡片

.card {
  background-color: var(--card-background);
  box-shadow: 0 0 55px rgba(38, 40, 45, .75);
  width: 75vw;
  max-width: 500px;
  border-radius: 4px;
}

图 6.5 显示了应用到我们项目中的样式。在卡片上添加了一些基本样式(我们将在本章后面继续添加),让我们将卡片垂直和水平放置在屏幕中间。

图 6.5 开始设置卡片样式

为了将卡片居中在屏幕的中间,我们将使用弹性布局(有时称为flexbox),它允许我们将元素放置在单个轴上,无论是垂直还是水平。虽然我们可以使用grid来定位卡片(而是否应该使用grid是一个个人偏好的问题),在这个例子中,我们只关心居中项目,而不是其在列和行中的位置,所以 Flexbox 似乎是一个更好的选择。

值为flexdisplay属性用于应该使用 Flexbox 放置在屏幕上的子元素的父元素。在我们的项目中,被定位的元素是卡片,其父元素是<body>元素,所以我们将添加display: flex声明到我们的body规则中。

接下来,我们定义我们希望<body>内的元素如何表现。在我们的例子中,我们有一个子元素(卡片),我们希望它居中。为了水平居中卡片,我们在body规则中添加一个justify-content: center声明。这个属性允许我们指定元素如何在我们的轴上分布。图 6.6 分解了选项。

图 6.6 justify-content属性的值

我们还希望垂直居中卡片。对于垂直定位,我们将使用align-items: centeralign-items属性使我们能够指定元素相对于彼此以及相对于容器的定位方式,如图 6.7 所示。

图 6.7 align-items属性的值

以下列表显示了我们的更新后的 body 规则。记住,被定位的元素的父元素是我们应用 flexbox 相关声明的目标。

列表 6.5 居中卡片

body {
  ...
  display: flex;
  justify-content: center;    ①
  align-items: center;        ②
}

① 水平居中卡片

② 垂直居中卡片

现在我们的卡片已经居中(图 6.8),让我们专注于卡片的内容,从个人资料图片开始。

图 6.8 居中卡片

6.5 样式化和定位个人资料图片

我们目前有一个矩形图像。我们希望将其变成圆形。我们还希望它在卡片上居中,并且稍微突出顶部。让我们首先将图像转换为圆形。

6.5.1 对象填充属性

圆形的高度等于其宽度,所以正如我们在图 6.9 中看到的,如果我们将图片的高度和宽度设置为等于我们的图像大小变量,图片将会扭曲。

图 6.9 扭曲的个人资料图片

为了防止图像扭曲,我们还必须指定图像相对于其给定大小的行为。为此,我们将使用object-fit属性。通过将object-fit的值设置为cover,我们指示图像保持其初始宽高比,但适应可用空间。在这种情况下,由于图像的高度大于宽度,我们将会失去一些图像的顶部和底部。

当我们使用object-fit时,图像默认居中,如果图像的一部分被裁剪,那么这些部分是边缘,这对于我们当前的使用案例和图片来说效果很好。但如果我们想调整图像在其分配大小内的位置,并且只从底部裁剪,我们就会添加一个object-position声明。

要使我们的图像成为 200 像素宽的圆形,我们使用列表 6.6 中的 CSS。记住,我们在body中设置了图像大小作为 CSS 自定义属性,因此我们将图像的宽度和高度设置为等于--imageSize变量。我们添加了object-fit声明以防止图像扭曲。最后,我们给图像一个 50%的border-radius使其成为圆形。

列表 6.6 居中卡片

body {
  ...
  --imageSize: 200px;
}

img.portrait {
  width: var(--imageSize);
  height: var(--imageSize);
  object-fit: cover;         ①
  border-radius: 50%;        ②
}

① 防止扭曲

② 使图像成为圆形

现在我们的图像看起来像图 6.10。

图 6.10 圆形个人资料图片

接下来,我们需要定位我们的图片。

6.5.2 负边距

为了将我们的图片定位在卡片上方突出,我们将使用负边距。为了将一个元素向下移动并远离其上方的内联内容,我们可以向元素添加一个正的margin-top值。但如果我们添加一个负边距,而不是被推下,元素将被拉上。我们将使用边距与文本居中结合来定位图片。查看图 6.11 中的最终设计,我们会注意到所有文本也是居中的。

图片

图 6.11 最终设计

因为所有文本都是居中的,让我们在卡片规则中添加一个text-align: center声明。图片默认是内联元素,所以我们会注意到,通过居中文本,图片也会居中(图 6.12)。

图片

图 6.12 居中文本

现在剩下的就是添加负顶部边距来将图片向上移动。我们希望图片的三分之一从顶部突出出来,我们将使用calc()函数来帮我们做数学计算。我们的函数是calc(-1 * var(--imageSize) / 3);。我们将图片大小除以 3 得到图片高度的三分之一,然后乘以-1 使其变为负数。我们的边距将使图片的三分之一从卡片的顶部突出出来,如图 6.13 所示。

图片

图 6.13 定位图片

接下来,我们需要给我们的卡片添加一些边距。由于我们添加到图片上的负边距,如果我们有一个短屏幕(图 6.14),图片的顶部就会消失在屏幕外。

图片

图 6.14 当窗口高度较小时裁剪图片的顶部

为了防止当窗口不是特别高时裁剪图片的一部分,我们想在卡片本身上添加一些垂直边距——一个大于或等于图片突出于卡片的部分的边距。为了计算突出的量,我们使用了calc(-1 * var(--imageSize) / 3);。对于我们的卡片边距,我们将使用类似的概念,取图片高度的 1/3,然后添加 24 像素来将卡片和图片移离边缘。我们的最终函数将是calc(var(--imageSize) / 3 + 24px)。以下列表显示了添加定位图片的 CSS。

列表 6.7 定位图片

.card {
  ...
  text-align: center;
  margin: calc(var(--imageSize) / 3 + 24px) 24px;   ①
}

img {
  width: var(--imageSize);
  height: var(--imageSize);
  object-fit: cover;
  border-radius: 50%;
  margin-top: calc(-1 * var(--imageSize) / 3);      ②
}

① 图像大小的三分之一加 24 像素的垂直边距和 24 像素的水平边距

② 负顶部边距使图片突出于卡片

在我们的图片定位和添加了边距,以确保在小屏幕上图片的顶部不会被裁剪(图 6.15)之后,让我们将注意力转向图片下面的弯曲红色背景。

图片

图 6.15 添加的卡片边距

6.6 设置背景大小和位置

要添加图片后面的红色弯曲背景,我们需要将以下列表中的声明添加到卡片规则中。

列表 6.8 定位图片

.card {
  background-color: var(--card-background);
  ...
  background-image: radial-gradient(
    circle at top,
    var(--primary) 50%,
    transparent 50%,
    transparent
  );
  background-size: 1500px 500px;
  background-position: center -300px;
  background-repeat: no-repeat;
}

让我们分解一下这段代码的功能。首先,我们添加了一个由图 6.16 所示的radial-gradient组成的background-image

图 6.16 使用radial-gradient添加背景

结合背景颜色和图片

我们可以向同一个元素添加背景颜色和背景图片。我们将颜色分配给background-color属性,将图片分配给background-image属性。或者我们可以在background简写属性中同时应用它们,如下所示:background: white url(path-to-image);

radial-gradient接受一个结束形状(圆形或椭圆形),然后定义我们想要每个颜色开始和结束的位置以形成渐变。我们将其定义为radial-gradient(circle, var(--primary) 50%, transparent 50%, transparent);

我们的主要颜色是红色,所以我们的渐变将创建一个直到达到容器 50%大小的红色圆圈。在容器大小的 50%处,颜色立即变为透明。因为颜色的变化是立即的,所以没有渐变效果,我们得到了一个干净整洁的圆圈。

默认情况下,径向渐变从其容器的中心发散,所以接下来我们在radial-gradient函数的开始处添加circle at top,以将圆圈的起点从背景中心移动到顶部。我们更新的radial-gradient函数是radial-gradient(circle at top, var(--primary) 50%, transparent 50%, transparent);(图 6.17)。

图 6.17 使渐变从容器顶部中心发散

现在我们希望将圆圈向上移动,使得圆圈的底部直接位于图片下方。图 6.18 显示,如果我们将背景向上移动-150 像素,并且我们的卡片相当短(我们的个人资料没有很多内容),那么圆圈和卡片边缘之间会在顶部角落出现间隙,这是我们不想看到的。

图 6.18 修改背景位置

为了防止这种情况发生,我们将背景图片设置为最大卡片尺寸的三倍宽度:(3×500 =1500)。当我们使用渐变创建background-image时,背景图片会随着容器的大小而增长和缩小,因此我们也将为背景设置一个固定高度。这样,无论卡片中有多少内容,我们背景的形状都是可预测的(图 6.19)。

图 6.19 编辑background-size和处理background-repeat

在更改背景尺寸后,我们还增加了移动背景向上移动的量,以便它直接位于个人资料图像下方。最后,如本章前面所述,背景图像默认情况下会重复。通过将图像向上移动,我们为背景平铺留出空间。我们只想有一个半圆,因此我们添加了一个 background-repeat 声明,其值为 no-repeat。现在我们的卡片背景定义如下所示。

列表 6.9 定位图像

.card {
  background-color: var(--card-background);
  ...
  background-image: radial-gradient(    ①
    circle at top,                      ①
    var(--primary) 50%,                 ①
    transparent 50%,                    ①
    transparent                         ①
  );                                    ①
  background-size: 1500px 500px;        ②
  background-position: center -300px;   ③
  background-repeat: no-repeat;         ④
}

① 创建一个平边为卡片顶部的半圆

② 设置背景图像的尺寸为 1500px 宽和 500px 高

③ 将背景水平居中并从卡片上方开始 300px

④ 防止背景平铺

图 6.20 展示了添加到卡片中的背景。随着我们卡片的顶部开始看起来不错,让我们专注于其余的内容。

图片

图 6.20 完成的背景图像

6.7 内容样式

我们当前的卡片没有任何填充,这意味着如果名字更长,它可能会潜在地延伸到卡片的边缘。在大多数情况下,我们会创建一个卡片作为组件或模板,以便为多个客户重复使用,因此让我们添加一些左右填充以确保文本不会延伸到卡片的边缘。我们还将添加一些底部填充,以便将链接和底部远离卡片的底部边缘。

列表 6.10 显示了我们的更新后的卡片规则,图 6.21 显示了新的输出。我们使用了填充简写属性,它定义了三个值:它声明顶部填充为 0,左右为 24px,底部填充为 24px。我们特别没有添加顶部填充,因为这会推下图像,迫使我们重新调整图像定位。

列表 6.10 向卡片添加填充

.card {
  ...
  padding: 0 24px 24px;
}

图片

图 6.21 添加卡片填充

6.7.1 名称和职位

沿着卡片向下,我们看到第一部分内容是名字。作为一个 <h1>,它有一些浏览器提供的默认样式,包括一些边距。我们将编辑边距以增加标题和图像之间的空间,并移除底部边距,以便职位标题直接出现在名字下方。我们还将颜色改为红色,并将字体大小设置为 2rem

rem 单位

一个 rem 是基于根元素的字体大小的相对单位——在我们的例子中,是 HTML。对于大多数浏览器,默认值是 16px。我们没有在我们的项目中设置 html 元素的字体大小;因此,当我们设置 <h1>font-size2rem 时,输出大小是 32px,假设默认值为 16px

使用相对字体大小,如 remem 的好处是可访问性。这些大小有助于确保文本无论用户的设置或设备如何都能优雅地缩放。

对于职位标题,我们将增加字体的大小和粗细,并将字体颜色更改为我们的次要颜色,即灰色。以下列表显示了我们的新规则,图 6.22 显示了输出结果。

列表 6.11 命名样式

h1 {                            ①
  font-size: 2rem;              ①
  margin: 36px 0 0;             ①
  color: var(--primary);        ①
}                               ①

.title {                        ②
  font-size: 1.25rem;           ②
  font-weight: bold;            ②
  color: var(--secondary);      ②
}                               ②

① 命名的样式

② 职位标题的样式

图 6.22 样式化姓名和职位标题

接下来,我们将对帖子、点赞和关注者信息进行样式化。

6.7.2 space-around 和 gap 属性

在我们的 HTML 中,描述列表(dl)包含职位、点赞数和关注者数(见 6.12 表)。每个分组都包含在一个<div>中,因此我们将为定义列表应用display值为flex以水平对齐所有三个分组。然后我们将justify-content属性设置为space-around以在卡片上分散它们。

列表 6.12 描述列表 HTML

<dl>
  <div>
    <dt>Posts</dt>
    <dd>856</dd>
  </div>
  <div>
    <dt>Likes</dt>
    <dd>1358</dd>
  </div>
  <div>
    <dt>Followers</dt>
    <dd>1257</dd>
  </div>
</dl>

space-around值通过在每个元素之间提供相等的空间并在每个边缘提供一半的空间,均匀地将元素分布在我们轴上。图 6.23 显示了间距的应用方式。

图 6.23 space-around属性

列表 6.13 显示了我们的描述列表样式。注意我们包括了gap: 12px声明,这确保了元素之间最小空间为 12 像素。我们可以在描述列表内的<div>上添加边距,但边距会影响外部边缘。gap属性仅影响元素之间的空间。

注意:gap属性在 iOS 14.5 及以后的版本中得到支持。在撰写本文时,许多人仍在使用早期版本。要检查该属性的全球使用情况,请参阅caniuse.com/flexbox-gap

列表 6.13 命名样式

dl {
  display: flex;
  justify-content: space-around;
  gap: 12px;
}

如图 6.24 所示,现在我们的个人资料统计数据在卡片上成行且均匀分布。

图 6.24 对齐的个人资料统计数据

然而,数字是偏移的。这种偏移来自描述,它有一些来自浏览器默认设置的边距。让我们去除这些设置,并使用以下列表中的 CSS 将文本样式设置为粗体、更大和红色。

列表 6.14 描述详情规则

dd {
  margin: 0;
  font-size: 1.25rem;
  font-weight: bold;
  color: var(--primary);
}

移除边距(图 6.25)后,我们注意到点赞数仍然没有在卡片上居中。

图 6.25 描述列表对齐

点赞数没有居中的原因是三个元素宽度并不完全相同。当元素分布时,浏览器会计算每个元素所需的总空间,并将剩余空间平均分配。因此,因为包含关注者的<div>比包含帖子的<div>大,所以点赞的<div>没有落在中间。

6.7.3 flex-basis 和 flex-shrink 属性

要居中对齐点赞,我们将为所有三个 <div> 分配相同的宽度。然而,不使用 width 属性,我们将使用 flex-basis 并将其值设置为 33%flex-basis 设置浏览器在计算元素所需空间时应使用的初始大小。我们还将 flex-shrink 设置为 1

flex-shrink 决定了如果容器中没有足够的空间容纳元素,元素是否允许缩小到小于由 flex-basis 值指定的尺寸。如果 flex-shrink 值为 0,则大小不会调整。任何正数值都允许调整大小。

我们将 flex-basis 设置为 33%。但请记住,我们还为每个元素之间设置了 12 像素的 gap。因此,当考虑到 gap 设置时,我们设置的 flex-basis 大小对于容器来说太宽了。通过允许元素缩小,我们告诉浏览器从每个 <div> 占据容器宽度的 33% 开始其定位计算,并将 <div> 均匀缩小以适应可用空间。这种情况防止我们不得不进行数学计算,以确定 <div> 应该有多宽才能保持相等的大小。

要编写我们的规则(列表 6.15),我们通过使用子组合器(>)针对描述列表(dl)的直接子 <div>,并应用 flex-basisflex-shrink 声明。

列表 6.15 居中对齐点赞

dl > div {
  flex-basis: 33%;
  flex-shrink: 1;
}

在我们的点赞居中(图 6.26)后,让我们将注意力转向定义术语(dt)。

图片 6.26

图 6.26 居中对齐的点赞

6.7.4 flex-direction 属性

在我们的原始设计中,描述细节(数字)位于描述术语上方。为了在视觉上翻转它们,我们将使用 flex-direction 属性。我们断言 Flexbox 可以在单个轴上放置元素。到目前为止,我们已经在水平轴(x 轴)上完成了我们的工作。

要将细节移动到术语上方,我们将使用垂直(y 轴)上的 Flexbox,有时称为 blockcross 轴。要更改 Flexbox 应该操作的轴,我们使用 flex-direction 属性。默认情况下,该属性具有 row 值,这使得 Flexbox 在 x 轴上操作。通过将值更改为 column,我们使其在 y 轴上操作。

此外,flex-direction 属性允许我们指定元素应该如何排序。将值设置为 column-reverse 告诉浏览器我们希望在 y 轴上操作,并且我们希望元素以反向 HTML 顺序排列,使描述细节(<dd>)首先出现,然后是描述术语(<dt>)。

如前所述,我们希望在父元素上设置行为——在本例中是 <div>。我们将向之前的 <div> 规则中添加以重新排序元素(列表 6.16)。我们还减小了描述术语(<dt>)的大小,以强调数字而不是其术语。

列表 6.16 反转内容顺序

dl > div {
  flex-basis: 33%;
  flex-shrink: 1;
  display: flex;
  flex-direction: column-reverse;
}
dt { font-size: .75rem; }

可访问性问题和内容显示顺序

由于可访问性的原因,我们想要确保我们编写的 HTML 顺序与它在屏幕上显示的顺序一致。如果一个用户在视觉上跟随页面内容的同时让电脑朗读页面内容,如果朗读的内容与他们看到的不匹配,他们可能会感到容易迷失方向或困惑。在使用flex-direction等属性重新排序内容时要谨慎。

图 6.27 显示了我们的样式化描述列表(<dl>)。

图 6.27 样式化描述列表

继续向下查看卡片,让我们将注意力转向位于个人资料统计信息下方的摘要段落。

6.7.5 段落

段落看起来已经很好了。我们打算对其做的唯一一件事是添加一些垂直边距以留出空间,并增加行高以提高可读性,如列表 6.17 所示。

注意,行高没有单位。通过不设置单位,我们允许行高与字体大小成比例缩放。这个无单位的值是特定于line-height属性的。如果我们将其设置为12px的值,例如,行高将保持 12 像素,无论字体大小如何。所以如果字体大小大幅增加,我们的字母会在垂直方向上重叠。始终不声明单位是最安全的。

列表 6.17 段落规则

p.summary {
  margin: 24px 0;
  line-height: 1.5;
}

在处理完段落(图 6.28)后,让我们样式化技术列表。

图 6.28 样式化摘要段落

6.7.6 flex-wrap属性

首先,我们将对列表元素本身进行样式化。我们将使用一种有时被称为药丸芯片标签的设计模式,其中元素具有背景颜色和圆角。我们的 CSS 将类似于列表 6.18。我们还包括一些填充,以便文本不会紧挨着标签的边缘。

列表 6.18 样式化列表元素

ul.technologies li {
  padding: 12px 24px;
  border-radius: 24px;
  background: var(--technologies-background);
}

在对单个元素进行样式化(图 6.29)后,我们可以专注于列表的布局。

图 6.29 样式化列表项

首先,我们将通过使用list-style: none来移除项目符号。然后我们将移除所有填充,并将垂直边距设置为24px,水平边距设置为0

为了定位项目,我们将使用 Flexbox,添加12pxgap,并将justify-content属性值设置为space-betweenspace-betweenspace-around类似,但它不会在容器的开始和结束处添加空间,如图 6.30 所示。

图 6.30 比较space-aroundspace-between

我们用于布局芯片的规则将类似于下一个列表。

列表 6.19 样式化技术列表

ul.technologies {
  list-style: none;
  padding: 0;
  margin: 24px 0;
  display: flex;
  justify-content: space-between;
  gap: 12px;
}

然而,当我们缩小屏幕宽度(图 6.31)时,我们注意到最后一个标签超出了我们的卡片。

图 6.31 标签超出卡片宽度

在窄屏幕上,我们的列表比卡片宽。为了防止内容溢出卡片,我们可以使用flex-wrap属性。

默认情况下,即使容器太小,flex 项目也会以直线显示,正如我们在技术列表中遇到的情况。为了在空间不足时将最后一个元素强制换行,我们可以将flex-wrap属性设置为wrap。此设置告诉浏览器在空间不足时在下方开始新的一行项目。

flex-direction一样,flex-wrap可以改变元素显示的顺序,但在这里我们不需要改变它。以下列表包含我们的更新规则。

列表 6.20 添加flex-wrap

ul.technologies {
  list-style: none;
  padding: 0;
  margin: 24px 0;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

注意图 6.32 中 CSS 和可访问性标签之间的间隔,尽管我们的列表元素没有任何边距。我们的列表有一个gap属性值为12px,这意味着我们将在项目之间有至少 12 像素的水平间隔,而且当换行时,我们将在项目垂直方向上添加 12 像素的间隔。

图片

图 6.32 窄屏幕上包裹芯片

6.8 样式化操作

在我们的个人资料卡片中,我们需要最后样式化的东西是用户可以在卡片底部执行的两个操作:发消息或关注个人资料所有者。尽管这些操作在语义上不同——一个是链接,另一个是按钮——但我们将使两者看起来都像按钮。让我们从适用于两者的基本样式开始。我们创建了一个规则,其中包含适用于两者的选择器,以确保这两种元素类型在视觉上保持一致。然后我们为它们各自不同的地方创建单独的规则。

我们还创建了一个focus-visible规则,将通过通用选择器(*)和伪类:focus-visible应用于所有元素,这样当用户通过键盘导航到我们的链接和按钮时,元素周围会出现一个虚线轮廓,他们可以清楚地看到他们即将选择的内容。以下列表展示了我们的样式。

列表 6.21 添加flex-wrap

.actions a, .actions button {         ①
  padding: 12px 24px;
  border-radius: 4px;
  text-decoration: none;              ②
  border: solid 1px var(--primary);
  font-size: 1rem;
  cursor: pointer;
}

.follow {
  background: var(--primary);
  color: var(--primary-contrast);
}

.message {
  background: var(--primary-contrast);
  color: var(--primary);
}

*:focus-visible {
  outline: dotted 1px var(--primary);
  outline-offset: 3px;
}

① 适用于链接和按钮

② 移除下划线

注意到在我们的基本样式表中,我们将链接和按钮的cursor都更改为pointer。在大多数浏览器中,链接默认会使用pointer,但按钮则不会。因为我们希望这两个元素有相似的用户体验,我们将定义cursor以确保一致性。图 6.33 展示了我们的样式化链接和按钮。

图片

图 6.33 样式化操作

然而,由于这两个按钮非常靠近,我们希望在它们之间添加一些空间。让我们最后一次使用flexgap来定位我们的操作元素。

我们将给列表设置 display 属性值为 flex 并添加 16pxgap。为了使两个元素居中,我们将使用 justify-content 属性并设置值为 center。最后,我们将通过给列表设置 margin-top 值为 36px 来在技术列表和我们的操作之间添加一些空间,如下所示。

列表 6.22 定位链接和按钮

.actions {
  display: flex;
  gap: 16px;
  justify-content: center;
  margin-top: 36px;
}

通过这个最后的规则,我们已经完成了个人资料卡的样式设计。最终产品如图 6.34 所示。

图片

图 6.34 完成的个人资料卡

摘要

  • CSS 自定义属性允许我们设置可以在整个 CSS 中重用的变量。

  • CSS Flexbox 布局模块允许我们在单个轴上(水平或垂直)定位元素。

  • flex-direction 设置 Flexbox 将在哪个轴上操作。

  • flex-directionflex-wrap 都可以改变元素显示的顺序。

  • align-items 属性设置元素相对于彼此在轴上的对齐方式。

  • justify-content 属性决定了元素如何定位;剩余空间将在应用该属性的元素内部进行分配。

  • flex-basis 为浏览器设置一个起始元素大小,用于布局伸缩内容。

  • flex-shrink 决定了当元素被伸缩时内容是否可以收缩以及如何收缩。

  • 通过使用 object-fit 属性,我们可以防止在固定高度和宽度与图片的宽高比不匹配时图片变形。

7 充分利用浮动的全部力量

本章涵盖

  • 使用浮动创建首字母下沉

  • 使用浮动将文本环绕图像

  • 使用 CSS 形状使文本跟随浮动图像的形状

网格和 Flexbox 使我们能够创建以前难以实现,甚至不可能实现的布局。最常见的一个例子是三列布局,其中所有三列的高度都相同,无论内容如何。另一种布局技术,与网格和 Flexbox 类似,已经存在了相当长的时间,就是浮动。作为 CSS 逻辑属性和值模块的一部分,浮动是专门为允许其他内容围绕被浮动的元素进行环绕而构建的;因此,它在处理文本中的图像和创建首字母下沉方面表现出色。

首字母下沉是一种用于样式化和强调文本的方法。它包括创建一个更大的(有时更华丽的)大写字母,通常位于页面或段落的开始处。在中世纪的装饰手稿中,首字母下沉经常被使用。图 7.1 段落开头的F就是 Carmina Burana 手稿中首字母下沉的一个例子。后来,随着印刷机的出现,这一概念被带到了印刷领域;印刷商创建了专门的符号和版面或简单地使用了更大的字体大小。在网络上,首字母下沉相对较少,但它们绝不是不可能创建的,而且它们是使我们的在线排版更加有趣的好方法。

图 7.1 Carmina Burana 手稿中段落开头的首字母下沉

另一种使内容更具视觉冲击力的方法是调整我们的图像,使其在文本中看起来很合适。当我们向内容添加图像时,我们通常会添加我们的图像元素和可能的一些边距,而不会过多地考虑这个过程。然而,结合使用 CSS 形状和浮动,我们可以使文本围绕图像的实际形状进行环绕,从而创建一个更加引人注目的效果。我们可以将文本围绕我们创建的几乎任何形状进行环绕,甚至是曲线。

在本章中,我们将仔细审视我们的排版和图像,使我们的内容更具视觉吸引力,同时确保其可访问性。我们将从一个未加样式的摘录开始,摘自杰克·伦敦的《野性的呼唤》(mng.bz/61WR)。我们将使用浮动为我们的第一段添加首字母下沉。然后我们将文本围绕我们的图像(栅格和矢量)进行环绕,遵循图像的内容。图 7.2 显示了起点和成品。

图 7.2 起点(左)和成品(右)

注意:栅格图像是通过使用像素网格创建的,而矢量图像是通过数学公式绘制的。关于栅格和矢量之间差异的深入了解,请参阅第三章。

列表 7.1 和列表 7.2 分别包含我们将在此章节中构建的页面的起始 HTML 和 CSS。要跟随我们为页面添加样式的过程,您可以从 GitHub 仓库 mng.bz/oJXD 或 CodePen codepen.io/michaelgearon/pen/MWodXxM 下载起始代码。我们的 HTML 包含一个 <main> 元素,其中包含一个标题 (<h1>)、块引用 (<blockquote>)、三个段落 (<p>)、两个图像 (<img>)和来源引用 (<cite>)。

列表 7.1 起始 HTML

<main>
  <h1>Chapter I: Into the Primitive</h1>
  <blockquote>"Old longings nomadic...</blockquote>
  <p>Buck did not read the newspapers, or he...</p>
  <img class="compass" src="./img/compass.png"                     ①
       width="175" height="175" alt="a black and gray compass">    ①
  <p>Buck lived at a big house in the...</p>
  <img class="dog" src="./img/dog.svg"                             ②
       width="126" alt="line drawing of a dog">                    ②
  <p>And over this great demesne Buck ruled...</p>
  <cite>London, Jack...</cite>
 </main>

① 方位图像

② 狗图像

我们的 CSS 包含一些基础样式来设置我们的页面,包括 marginpaddingbackground-colorbody 的宽度限制为 78ch,并且当屏幕宽度超过最大值时,边距会居中内容。我们还设置了页面的默认字体,即 Times New Roman。最后,为了确保在小屏幕上图片不会溢出,我们给它们设置了最大宽度,设置为 100%。换句话说,图片的宽度不能超过其容器。

注意 注意到我们使用 ch 作为 max-widthch 是一个基于所使用的字体家族的相对单位。1ch 等于或更准确地说,是符号 0(零)的宽度或水平占用空间。

列表 7.2 起始 CSS

html {
  padding: 0;
  margin: 0;
}

body {
  background-color: rgba(206, 194, 174, 0.24);
  padding: 4rem;
  font-size: 16px;
  max-width: 78ch;                                 ①
  margin: 0 auto;                                  ②
  font-family: 'Times New Roman', Times, serif;
  border-left: double 5px rgba(0,0,0,.16);
  min-height: 100vh;                               ③
  box-sizing: border-box;
}
img {
  max-width: 100%;
}

① 防止我们的内容变得过于宽

② 居中内容

③ 不论窗口大小,背景覆盖整个窗口。

7.1 添加首字母下沉

我们有一些基础 CSS 来设置页面样式,因此现在我们将关注文本。由于我们身体的宽度被限制在一个适合文本的宽度上,我们不需要担心行长。但我们确实需要处理行距。

7.1.1 行距

行距(发音为’le-diŋ)是行与行之间的空间。这个术语来自印刷机时代,当时排版工人使用各种宽度的铅条来调整文本行之间的间距。我们将使用 line-height 属性来实现相同的效果。这个属性可以接受一个数值(line-height: 2)或一个带单位的数值(line-height: 5px)。单位可以是相对的,如 ems,也可以是固定的,如像素。除非我们提供的单位相对于字体大小(例如 em),如果字体被缩放或子元素有不同的字体大小,行高可能看起来不正确,并可能对可读性产生负面影响。当我们使用无单位的数值时,行高会自动相对于元素的字体大小计算,从而消除这种担忧。因此,我们将使用无单位的 line-height。我们将通过创建一个专门针对段落元素的规则,并将高度设置为以下方式来设置所有段落的 line-heightp { line-height: 1.5; }

提示 研究表明,行高在 1.52 之间的文本对于认知障碍人士来说更容易进行行追踪(www.w3.org/TR/WCAG20-TECHS/C21.html)。

7.1.2 对齐

当文本紧随图像时,为了达到最佳效果,我们将使文本对齐。对齐文本意味着我们将使所有行具有相同的宽度——这是一种常用于报纸上使文本列的右边缘整齐而不是参差不齐的技术。

警告 网络内容可访问性指南(WCAG)包括三个相互依赖的符合级别:A、AA 和 AAA。A 是最不限制的,AAA 是最严格的。大多数情况下,网站的目标是达到 AA 级别的符合。但如果我们必须符合 AAA,值得注意的是,对齐文本违反了可访问性指南 1.4.5,这是 AAA 的要求(mng.bz/v1ja)。

为了使文本对齐,我们将使用 text-align 属性,它可以接受 leftrightcenterjustify 的值。我们将向段落规则添加 text-align: justify;。现在该规则有两个属性,text-alignline-height,负责段落样式。以下列表显示了完成的段落规则,图 7.3 显示了结果。

列表 7.3 完成的段落规则

p {
  line-height: 1.5;
  text-align: justify;
}

图 7.3 样式化段落

段落处理完毕后,我们可以专注于第一段的第一字母来创建我们的首字母下沉。

7.1.3 第一字母

我们不需要向 HTML 中添加任何元素来选择第一段的第一字母。我们可以使用伪类 :first-of-type 来选择第一段,然后使用伪元素 ::first-letter 来获取字母,在这种情况下是一个 B,这两个都可以链式使用。在代码中,这些选择器转换为 p:first-of-type::first-letter {}

注意 伪类被添加到选择器中以针对特定状态;伪元素允许我们选择元素的一部分。

字母被选中后,我们可以开始对其进行样式设计,使其看起来像首字母下沉。为了使其从其余文本中脱颖而出,我们将选择一个更华丽的字体。在这种情况下,我们将从 Google Fonts 导入 Passions Conflict (mng.bz/X5vE;图 7.4)。

图 7.4 激情冲突符号

由于这种字体具有特别华丽的字母,它非常适合用作首字母。我们还将在此章的后面使用它来样式化文本开头处的引言。使用如此美丽的字体,如这一种,是装饰页面的一种美妙方式——但仅适用于短篇内容。手写体和显示字体可能很难阅读,因此它们不适合大块文本。然而,对于首字母、大标题或短引言,这些字体可以使元素与内容区分开来,并为页面增添一些个性。

这种字体中的符号比我们用于其余内容的 Times New Roman(字体)要小得多。由于我们正在创建首字母,根据定义,它比其余文本要大,因此我们需要调整字体大小。我们还将调整字母的行高,使其与文本很好地匹配。最后,我们将第一个字母向左浮动,以便文本围绕字母流动,达到我们期望的效果。

float属性根据传递给它的值将元素放置在其容器右侧或左侧。根据 Mozilla 开发者网络的说法,“元素从页面的正常流程中移除,尽管仍然属于流程的一部分”(mng.bz/ydle)。围绕它的内联元素(我们的文本)使用剩余的空间来围绕浮动元素包裹。

float属性可以取三个值之一:leftrightnone(元素不浮动)。由于我们的文本是英文,从左到右流动,我们希望将字母B保持在左边,因此我们将通过添加float: left;到我们的规则中来将第一段的第一字母(B)向左浮动。以下列表显示了我们所创建的用于样式化首字母的完整 CSS 规则,以及导入 Passions Conflict 字体的过程。

列表 7.4 样式化和定位第一段的第一字母

@import url(
  'https:/ /fonts.googleapis.com/css2?
➥ family=Passions+Conflict&display=swap'                ①
);

p:first-of-type::first-letter {                          ②
  font-size: 6em;                                        ②
  float: left;                                           ②
  line-height: .5;                                       ②
  font-family: 'Passions Conflict', cursive;             ②
}                                                        ②

① 导入 Passions Conflict 字体

② 样式化我们第一段开头字母 B 的规则

注意,我们调整了第一字母的行高以调整B下面的空间。默认情况下,行高与字体大小成比例。由于我们的字母很大,所需的行高很高,因此我们将其降低以使文本在首字母下方流动得更自然。图 7.5 显示了生成的输出。

图 7.5 首字母

我们使用em和无单位的line-height,这样如果以后我们更改段落的字体大小,首字母将相应地缩放。6em的值是基于父元素的font-size设置的,在这种情况下是段落标签。

为了将我们的 B 重新定位以更好地与文本匹配,我们编辑了字母的 line-height。但我们可以使用另一种技术。我们可以将 Bposition 设置为 relative,然后使用 topbottomleftright 来改变其相对于文本其余部分的位置。在我们创建好首字下沉后,我们将把注意力转向页面开头的引言。

7.2 引言的样式

页面顶部的引言目前相当单调,并且在与文本的其余部分中有些迷失。为了使其突出,我们将使用我们用于首字下沉的相同字体。由于之前提到的尺寸和行高差异,我们将调整这些参数,以便段落和引言具有统一的大小和间距。列表 7.5 显示了我们将添加的 CSS 以完成此任务,图 7.6 显示了输出。

列表 7.5 <blockquote> 格式化

blockquote {
  font-family: 'Passions Conflict', cursive;
  font-size: 2em;
  line-height: 1;
}

图 7.6 样式 <blockquote>

再次,我们使用相对单位,以便如果其余内容的字体大小发生变化,引言也会发生变化。你可能已经注意到,尽管我们之前(第 7.1.1 节)提到,为了最佳的可读性,行高应为 1.52,但我们在这里使用了 line-height1。我们在这里做出例外,因为默认情况下,字体已经具有很大的行高;我们不需要增加大小。偶尔,我们会遇到默认情况下具有自然高行高的字体,尤其是在我们处理手写体或显示字体时。在这种情况下,有时我们必须根据字体的设计对行高可读性指南做出例外。

现在,我们的文本已经处理好了,我们可以专注于图像。

7.3 在指南针周围弯曲文本

我们需要做的第一件事是将指南针图像向右浮动,以便文本围绕它包裹。我们的指南针是一个 PNG 图像,因为它是一个矩形图像,所以文本在围绕图像包裹时遵循矩形路径。图 7.7 显示了浮动的指南针。已经应用了边框来显示其边界框。

图 7.7 正方形指南针

7.3.1 添加 shape-outside:圆形

要使文本跟随指南针的曲线,我们需要在图像中添加一个曲线,以便文本可以围绕它包裹。我们将使用的属性是 shape-outside。这个属性允许我们定义一个形状,相邻的文本将围绕这个形状流动。形状不必是矩形的;相反,它可以下列形状中的任何一个:

  • 圆形或椭圆形

  • 多边形

  • 从图像派生(使用图像的 alpha 通道 [透明度] 来确定形状应该是什么)

  • 路径(在规范中,但截至本文写作时,任何浏览器都没有实现;见 mng.bz/aMWX

  • 盒模型值(margin-boxcontent-boxborder-boxpadding-box

  • 线性渐变

由于我们有一个圆形图形,我们想要的目标形状是一个圆。这个决定给我们提供了几个选择:

  • 使用 CSS 形状(mng.bz/aMWX)。

  • 使用 border-radius

让我们先看看如何使用形状。为了定义我们的圆,我们将使用 circle() 函数,它接受一个可选的 radius 属性和一个可选的 position 属性来定义圆的中心开始的位置。如果没有提供 radius,则默认值为 closest-side。如果省略了 position 属性,则圆的起点默认为图像的中心:

circle(<shape-radius>, at <position> )

在我们的情况下,我们希望圆的中心位于图像的中间,所以我们不会传递一个 position 属性。然而,我们必须定义一个 radius,我们将将其设置为 50%

数学原理

我们希望半径等于我们图像宽度的一半,这在底层将解析为宽度平方加高度平方的平方根除以 2:

方程式 7-1

因为我们的图像是正方形,宽度为 175,当我们传递一个 radius50% 时,我们的半径为 87.5 是合乎逻辑的。但如果图像是矩形的,理解基于百分比的半径是如何计算的对于理解最终输出将是什么样子非常重要。

如果我们有一个高度为 100px、宽度为 300px 的风景图像,当选择基于百分比的值时,所需的半径需要更明显。我们可以使用以下公式来计算半径是多少:

方程式 7-2

图 7.8 显示了当我们使用 circle() 函数中的 50% 值时,半径将如何应用于我们的正方形图像与矩形图像。

图像 7-8

图 7.8 将半径应用于正方形图像与矩形图像

我们的图像是正方形,所以我们使用一个值为 circle(50%)shape-outside 属性来设置我们的图像。列表 7.6 显示了 CSS 规则。我们的图像是正方形,因此它的宽高比为 1(宽度/高度 = 175 ÷ 175 = 1)。

定义 图像的宽高比是通过将宽度除以高度计算出的图像高度和宽度的比例关系。

添加宽高比并不是严格必要的,但有助于减少加载时的布局偏移。

定义 当一个元素被添加到页面或其大小发生变化时,页面上的所有元素都会移动以腾出空间给元素或填充留下的空白。页面元素的运动被称为布局偏移

当图像具有固定的高度和宽度或已定义的宽高比时,浏览器可以在加载图像时为其预留空间,从而减少布局偏移。因此,定义图像的宽高比和/或高度和宽度是一个好的实践。

列表 7.6 shape-outside

img.compass {
  aspect-ratio: 1;                ①
  float: right;                   ②
  shape-outside: circle(50%);     ③
}

① 宽高比

② 将图像浮动到右侧

③ 添加一个值为 50%的圆

图 7.9 展示了我们的输出。文本围绕图像环绕并跟随曲线,但图像没有任何裁剪。这种效果之所以可行,是因为我们的图像有一个透明的背景。

图片

图 7.9 浮动指南针,文本弯曲

7.3.2 添加 clip-path

我们已经使文本弯曲,但图像仍然是方形的。如果我们给图像添加一个背景,这一点就会变得明显。要使图像看起来真正是圆形的,我们需要添加一个clip-pathclip-path属性也接受一个形状,所以我们将传递与传递给shape-outside相同的值。我们还将给我们的图像添加一些边距,以在图像和文本之间添加一些呼吸空间。列表 7.7 显示了我们的图像的完整 CSS。

列表 7.7 clip-path

img.compass {
  aspect-ratio: 1;
  float: right;
  shape-outside: circle(50%);
  clip-path: circle(50%);
  margin-left: 1rem;
}

我们添加了一个与shape-outside匹配的clip-path,并在图像的左侧添加了一些边距,以防止文本离图像太近,特别是因为指南针有从圆形轮廓突出来的箭头,而我们的circle()没有创建这样的轮廓。图 7.10 显示了最终的输出。

图片

图 7.10 圆形浮动指南针

当我们添加clip-path时,我们会观察到现在图像本身,包括背景,看起来是圆形的。角落已经被裁剪,之前是方形的背景现在是圆形的。此外,增加的边距使文本围绕我们的指南针箭头移动,使其看起来不那么拥挤。

我们已经展示了我们可以通过使用 CSS 形状来创建圆形。现在让我们看看如何使用border-radius来创建圆形。

7.3.3 使用 border-radius 创建形状

当我们使用border-radius来塑造元素时,我们可以从元素的轮廓创建一个 CSS 形状。我们仍然使用shape-outside,但不是传递一个形状,而是指定我们想要形状形成的盒模型级别。我们的选项有

  • margin-box—形状跟随边距。

  • border-box—形状跟随边框。

  • padding-box—形状跟随填充。

  • content-box—形状跟随内容。

让我们从一张干净的画布开始,将我们的图像浮动到右边,并添加一些边距以防止文本拥挤图像。列表 7.8 包含我们的起始 CSS,图 7.11 显示了当前的显示。

列表 7.8 起点

img.compass {
  aspect-ratio: 1;
  float: right;
  margin-left: 1rem;
}

图片

图 7.11 重置浮动并添加边距

现在,让我们添加一个border-radius50%,这将使我们的图像成为圆形。然而,在这个时候,文本并没有跟随曲线。我们仍然需要添加shape-outside属性。

我们的图像有一个边距,理想情况下我们希望形状尊重这个边距,因此我们将使用margin-box值。接下来的列表显示了这一概念在代码中的应用。

列表 7.9 添加border-radiusshape-outside

img.compass {
  aspect-ratio: 1;
  float: right;
  margin-left: 1rem;
  border-radius: 50%;
  shape-outside: margin-box;
}

图 7.12 显示了带有白色背景和边框的输出,以强调图像的形状。

图片

图 7.12 指南针形状,border-radius50%shape-outside值为margin-box

与我们使用 shape-outsidecircle() 函数时不同,我们的图像已经裁剪成圆形形状,消除了使用 clip-path 的需要。这个结果是我们使用 border-radius 的直接结果,它为我们做了裁剪。

我们已经看到了两种实现相同结果的不同方法。CSS 提供了多种方法来处理许多问题,包括这个问题。这两种选项并没有特别优于对方。border-radius 需要稍微少一点的代码,这给它带来了一丝优势,但在这个情况下,选择是一个个人偏好的问题。

现在我们已经处理了指南针图像,我们将继续将文本环绕在狗的周围。

7.4 将文本环绕在狗的周围

与标准形状的指南针不同,狗有一个不规则的轮廓。这张图像是由单一路径组成的线条艺术,所以我们可能会想从 SVG 文件中抓取路径并使用 path() 函数来创建我们的形状。然而,正如我们即将看到的,尽管它在 CSS 规范中定义了(www.w3.org/TR/css-shapes),这种技术不会起作用。

7.4.1 使用 path()...或者尚未

让我们打开图像文件并在编辑器中检查代码。以下列表显示了为了简洁而省略的图像代码,以突出重要信息。

列表 7.10 dog.svg

<svg  viewBox="0 0 152 193">
  <defs>
    <style>
      .cls-1{
        fill:none;
        stroke:#000;
        stroke-miterlimit:10;
        stroke-width:2px;
      }
    </style>
  </defs>
  <path class="cls-1" d="M21.9135,115.62c-17.2115,4.7607-37.3354,..."/>
</svg>

我们有 <defs> 元素,它包括图像的样式。这部分定义了 SVG 中的单个元素将看起来是什么样子。然后我们有一个 <path>,这是显示狗的元素。这个元素有 1,988 个字符长,相当复杂,当 shape-outside: path('M21.913...'); 被粘贴到 path() 函数中时,它似乎没有做任何事情。原因是当这本书被编写时,没有浏览器完全实现了 path()

当这个功能实现时,使用图形编辑器创建我们的路径并将它们复制以创建我们的形状将是一种有价值的技巧。但这种方法有一个缺点:路径可能会相当长,使得维护变得可疑。与此同时,我们有一些替代方案:

  • 创建一个大致匹配我们图像的多边形形状,类似于我们用于圆形的技术

  • 使用 url() 函数,它将图像拉入并基于 alpha 通道创建形状

我们将选择第二种选项:url() 函数。

7.4.2 浮动图像

正如我们在处理指南针图像时所做的(第 7.3 节),我们将首先浮动图像,但这次我们将将其浮动到左侧,以打破我们页面上的视觉单调性。然后,为了创建形状,我们将使用 url() 函数并将图像路径传递给它。列表 7.11 展示了应用于狗图像的 CSS。

提供图像文件

当使用shape-outside的 URL 时,我们需要确保我们的代码通过服务器运行,这样浏览器会获取图像,而不是直接从文件系统中读取。这种方法与跨源资源共享(CORS)和浏览器设置的安全策略相关。你可以在 CSS 规范中找到详细的解释,链接为mng.bz/pdMw

为了减轻这个问题,GitHub 仓库中的示例代码使用了http-server,在localhost:8080上提供文件服务以完成此任务。另一个选择是使用shape-outside: url("https:/ /raw.githubusercontent.com/michaelgearon/Tiny-CSS-Projects/ main/chapter-07/before/img/dog.svg")引用 GitHub 上的托管文件。

列表 7.11 狗向左浮动

img.dog {
  aspect-ratio: 126 / 161;
  float: left;
  shape-outside: url("https:/ /raw.githubusercontent.com/michaelgearon/Tiny-CSS-Projects/main/chapter-07/before/img/dog.svg");
}

我们将图像向左浮动,然后添加我们的shape-outside,传递对图像本身的引用。浏览器将查看图像的透明度,并根据透明度结束的位置确定创建形状的位置。图 7.13 显示了我们的输出。

图片

图 7.13 浮动狗

由于我们的图像有一个不透明的线条和透明的背景,裁剪是直接的。如果我们的图像有一个从不透明到透明的渐变,我们可以通过使用shape-image-threshold属性来调整裁剪。该属性接受介于0(完全透明)和1(完全不透明)之间的值。

7.4.3 添加 shape-margin

下一步是添加一些边距来将文本从图像推开,因为它看起来相当拥挤。我们不能简单地给图像添加边距,就像我们向右浮动时做的那样;如果我们尝试这样做,我们会注意到边距被忽略了。相反,我们需要使用shape-marginshape-margin属性允许我们调整形状和其余内容之间的空间量。我们将添加1em的空间,如以下列表和图 7.14 所示。

列表 7.12 将shape-margin添加到我们的规则中

img.dog {
  aspect-ratio: 126 / 161;
  float: left;
  shape-outside: url("https:/ /raw.githubusercontent.com/michaelgearon/Tiny-CSS-Projects/main/chapter-07/before/img/dog.svg");
  shape-margin: 1em;
}

图片

图 7.14 应用到图像的shape-margin

图像底部的文字仍然相当接近。在这个时候,我们可以添加一些边距来增加空间,只要添加的边距小于或等于shape-margin的数量。如果值大于shape-margin的数量,边距仍然会生效,但只会生效到shape-margin的数量。记住这个注意事项,我们将向图像的右侧添加1em的边距。下一个列表显示了狗图像完成的 CSS。

列表 7.13 完成的狗图像

img.dog {
  aspect-ratio: 126 / 161;
  float: left;
  shape-outside: url('img/dog.svg');
  shape-margin: 1em;
  margin-right: 1em;
}

shape-marginmargin-right的组合将文本从我们的图像推开,形成了我们在图 7.15 中看到的精致结果。

图片

图 7.15 完成的浮动狗

完成最后这一部分后,我们已经完成了页面的样式设计(图 7.16)。我们有一个视觉上吸引人且比我们开始时更有趣的布局。

图片

图 7.16 完成的布局

我们创建了一个由浮动的使用所实现的布局。如果我们使用 Flex 或 Grid,很难轻易地达到相同的效果。无论我们单独使用它,就像在我们的首字下沉示例中那样,还是与形状(虽然这些形状也相当新颖)结合使用,浮动继续是我们工具箱中的一个宝贵资产。

摘要

  • 行距,即行与行之间的空间,对于可读性很重要。

  • 可以将 Float::first-letter 结合使用来创建首字下沉。

  • 并非所有字体在给定相同大小值时都具有相同的大小和行高。

  • shape-outside 属性使用 CSS 形状来改变元素形状。

  • 使用 border-radius 可以创建圆形形状。

  • 与浮动的 CSS 形状相邻的内联内容将跟随形状。

  • 当我们使用 url()shape-outside 结合时,浏览器必须获取图像文件(托管或通过 http-server 或等效方式)。

  • shape-margin 属性设置了形状的边距。

  • 有些布局没有使用浮动是无法创建的。

8 设计结账购物车

本章涵盖

  • 使用响应式表格

  • 使用网格自动定位

  • 格式化数字

  • 基于视口大小通过媒体查询条件性地设置 CSS

  • 使用 nth-of-type() 伪类

我们中的许多人经常上网购买从食物到书籍再到娱乐等所有事物。这种体验的共同点是结账购物车。我们通过将所选项目添加到虚拟购物车或篮子中来做出选择,在最终购买之前我们可以查看所选项目。在本章中,我们将探讨如何对结账购物车进行样式化,使其在窄屏和宽屏上都能正常工作。我们还将探讨如何处理窄屏和宽屏的表格。表格对于显示数据非常有用,但它们在移动设备上可能有点难以样式化,因此我们将探讨窄屏的 CSS 解决方案。

首先,我们将处理一些主题。无论屏幕宽度如何,我们的输入字段、链接和按钮等元素看起来都一样,因此我们将首先对它们进行样式化。在组装用户界面的过程中早期定义一个主题可以显著减少冗余代码。它还增加了我们保持样式一致的能力,因此无论我们是在创建结账购物车还是任何其他页面或应用程序,我们都可以将此过程应用于任何数量的设计。

接下来,我们将专注于布局,从窄屏到宽屏。在窄屏设备上,如手机,我们倾向于堆叠东西。随着屏幕变大,我们添加规则来利用我们可用的全部宽度。通常,从移动布局开始并随着屏幕变宽添加样式比从宽屏布局开始并在屏幕变小时覆盖之前设置的布局元素更容易。

8.1 入门

我们将创建样式以适应三种屏幕尺寸:

  • 窄屏(大多数手机)—最大宽度为 549 像素

  • 中屏(平板电脑和小屏幕)—介于 500 到 955 像素之间

  • 宽屏(桌面电脑和高分辨率平板电脑)—宽度超过 955 像素

图 8.1 显示了每个屏幕尺寸的起始点和最终输出。

图 8.1 小、中、大屏幕的起始和结束输出

无论屏幕大小如何,我们都会使用相同的 HTML。我们将有一个样式表,并使用媒体查询来调整元素的外观,这取决于屏幕大小。我们的起始 HTML 在 GitHub 上 mng.bz/GRpJ 和 CodePen 上 codepen.io/michaelgearon/pen/ExmLNxL 可用。代码由两部分组成,一部分用于购物车,另一部分用于摘要,它们被包裹在一个容器中,我们将在宽屏上使用该容器将部分并排放置。购物车部分包括一个标题和一个包含购物车中每个项目的表格。摘要部分包含一个标题、一个描述列表和两个链接。图 8.2 示意图说明了 HTML 元素。

图 8.2 HTML 元素图

以下列表是我们开始时的 HTML 的简略版。

列表 8.1 起始 HTML

<body>
  <main>
    <h1>Checkout</h1>
    <div class="section-container">
      <section class="my-cart">
        <h2>My Cart</h2>
        <table>
          <thead>
            <tr>
              <th>Image</th>
              <th>Item</th>
              <th>Unit Price</th>
              <th>Quantity</th>
              <th>Total</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>
                <img src="./img/grapes.jpg" width="75" height="105"
                  loading="lazy" alt="Red grapes">
              </td>
              <td data-name="Item">Red Grapes, 1lb</td>
              <td data-name="Unit Price">$ 3.23</td>
              <td data-name="Quantity">
                <input name="grapes" type="number"
                   aria-label="Pounds of grape baskets"
                   min="0" max="99"
                   value="1">

              </td>
              <td data-name="Total">
                <!-- value calculated & inserted by JS -->
              </td>
              <td>
                <button type="button" class="destructive">
                  <img width="24" height="24" 
                   src="./img/icons/remove.svg" alt="remove grapes">
                </button>
              </td>
            </tr>
            ...
          <tfoot>
            <tr>
              <th colspan="4" scope="row">Total:</th>
              <td id="total">
                <!-- value calculated & inserted by JS -->
              </td>
            </tr>
          </tfoot>
        </table>
      </section>

      <section class="summary">
        <h2>Summary</h2>
        <dl>
          <dt>Number of Items</dt>
          <dd id="itemQty">
            <!-- value calculated & inserted by JS -->
          </dd>
      ...

        </dl>
        <div class="actions">
          <a href="#" class="button primary">
            Proceed to Checkout
          </a>
          <a href="#" class="button secondary">
            Continue Shopping
          </a>
        </div>
      </section>
    </div>
  </main>
  <script src="./script.js"></script>
</body>

除了起始的 HTML 代码,我们还会使用一个 JavaScript 文件(script.js)。我们不会编辑或与该文件交互;它仅仅用于更新总结部分的总额。

8.2 主题化

尽管我们的布局有两个明确定义的部分(购物车和总结)并且需要适应不同的屏幕尺寸,但某些样式无论它们在哪里或屏幕大小如何都不会改变。这些样式包括

  • 字体

  • 按钮和链接样式

  • 输入和错误信息样式

  • 标题大小和颜色

这些样式可以被称为我们的主题,为了在整个页面上保持一致性,我们通常希望只写一次并应用到每个地方。让我们从我们的字体开始。

8.2.1 字体排版

目前,我们的font-family是浏览器的默认字体。对于这个项目,我们将从 Google Fonts 导入 Raleway 并将其应用于 body。我们将导入常规和粗体,因为在这个项目中我们都需要它们。我们还将设置默认的文本颜色为#171717,这看起来几乎是黑色,用于我们的文本。我们在这个设计中不使用黑色,因为它是一个柔和的设计,纯黑色可能会显得相当刺眼。

接下来,我们将处理我们的数字。一个字体家族默认有旧式或现代数字。区别在于数字与平均线和基线的对齐方式,如图 8.3 所示。

图 8.3 旧式与现代图表对比

旧式数字有部分会超出基线;现代的则不会。因为我们正在创建一个购物车,我们想要堆叠数字以显示它们被添加以创建总额,我们希望使用现代数字以便它们对齐得很好。然而,我们选择的字体家族 Raleway 默认使用旧式数字。为了使我们的字体使用现代数字,我们可以使用font-variant-numeric属性,它允许我们设置我们希望数字如何显示。这个不太为人所知的属性在处理数字时很有用,因为它允许我们控制它们显示的多个方面,包括

  • 零是否以斜杠显示

  • 数字的对齐方式

  • 分数的显示方式

我们将使用font-variant-numeric: lining-nums属性,这将把我们的数字从旧式改为现代。图 8.4 显示了应用font-variant-numeric到我们的 body 规则之前和之后的总结部分。在之前的版本中,数字大小不同;在之后的版本中,它们对齐且大小均匀。

图 8.4 应用font-variant-numeric属性前后的对比

最后,我们将更改我们标题的颜色为青色。在这次更改之后,我们将为我们的页面设置基本的排版。我们直接在<body>元素中应用了它,以便页面内的其他子元素可以继承这些值。列表 8.2 显示了到目前为止我们构建的规则。

列表 8.2 应用于<body>元素的排版相关样式

@import url('https:/ /fonts.googleapis.com/css2?
➥ family=Raleway:wght@400;700&display=swap');

body {
  font-family: 'Raleway', sans-serif;
  color: #171717;
  font-variant-numeric: lining-nums;
}

h1, h2 {
  color: #2c6c69;
}

图 8.5 展示了我们的更新后的输出。

图 8.5 应用的排版

让我们把注意力转向链接和按钮。

8.2.2 链接和按钮

我们的页面有几个链接和按钮,但从风格上讲,所有这些元素看起来都像按钮。它们可以根据目的进行分类:

  • 主要行动号召——前往结账链接

  • 次要行动号召——继续购物链接

  • 破坏性——带有X的按钮用于从购物车中删除项目

我们将使用这些类别来命名我们的类,以便我们的规则可以轻松重用。

链接与按钮

在我们的项目中,我们既有链接也有按钮。使用哪一个并不是基于个人喜好;它基于预期的功能或目的。对于导航,我们应该使用链接。对于执行操作,例如从我们的购物车中删除项目,我们应该使用按钮。我们可以随意设计这些元素,但底层元素应该与预期的使用场景相匹配。

区分的原因是链接和按钮自动由浏览器关联了信息和行为。这些行为包括它们能够聚焦的能力,更重要的是,它们的角色。元素的角色被辅助技术用来帮助用户与页面交互。

链接和按钮行为差异的一个具体例子是用户能够右键点击它,在新标签页或窗口中打开链接。如果链接是用按钮和 JavaScript 创建的,那么这个功能对用户不可用。

在本章中,我们处理的是一个单独的页面,但这种情况是一个异常。在一个完整的应用程序中,我们有多个页面或组件将重用相同的样式,所以而不是将类命名为类似proceed-to-checkout的东西,我们将使用primary,这样类就可以在不同的上下文中轻松重用。

在我们讨论按钮类型之间的差异之前,让我们先考虑它们的相似之处,并为所有看起来像按钮的链接(这些链接被赋予了button类)和按钮本身制定一个基线。在制定基线之后,我们将为每种按钮类型制定规则。

为了创建我们的基线,我们将首先移除浏览器设置的默认灰色背景,我们将通过使用background: none来完成这项工作。我们还将更新paddingborderborder-radius的值。

最后,因为我们正在将此规则应用于链接和按钮,并且因为链接默认带有下划线,我们将通过将 text-decoration 属性设置为 none 来从链接中移除下划线。以下列表显示了我们的按钮和具有 button 类的链接的基本规则。

列表 8.3 按钮的基本样式

button, .button {      ①
  background: none;
  border-radius: 4px;
  padding: 10px;
  border: solid 1px #ddd;
  text-decoration: none;
}

① 此规则将应用于所有按钮元素以及所有具有 button 类的元素。

在处理完按钮的默认状态后,我们将添加样式变化,以应用于用户将鼠标悬停在按钮上或通过键盘聚焦在按钮上时。为了实现这一目标,我们将使用 :hover:focus 伪类。

注意:为了定位特定状态,会在选择器中添加一个伪类。在 hoverfocus 上添加样式变化对于可访问性很重要,因为它提供了视觉反馈,让用户知道他们可以与项目交互。对于键盘导航,focus 上的样式变化让用户知道他们即将与之交互的元素。没有这些视觉提示,很难知道我们可以点击什么,以及我们的焦点在哪里(mng.bz/zmdA)。

hover 状态下,我们将在按钮周围添加一个点状青色轮廓,为了给轮廓留出一些空间,我们将它偏移 2 像素。我们将使用两个属性:outlineoutline-offsetoutlineborder 类似,采用相同的三个属性,即 stylewidthcoloroutline-offset 采用一个 length 值(可以是负值),它决定了轮廓与元素边缘之间的空间量。

对于 focus,我们将使用与 hover 相同的样式,但我们将线条改为实线。以下列表显示了我们的最终 CSS,用于 hoverfocus 状态。

列表 8.4 按钮的 hoverfocus 状态

button:hover,
.button:hover {
  outline: dotted 1px #2c6c69;
  outline-offset: 2px;
}

button:focus,
.button:focus {
  outline: solid 1px #2c6c69;
  outline-offset: 2px;
}

图 8.6 显示了 hoverfocus 状态的样式化链接和按钮。

图 8.6 按钮的基本样式,包括 hoverfocus

现在我们有了基线,我们可以开始关注每个单独的使用案例。我们将从我们的调用行动(继续结账和继续购物链接)开始。因为我们已经设置了基线,我们只需要编辑这些案例的颜色,如列表 8.5 所示。我们将根据我们更喜欢(或期望)用户选择的操作来区分这两个动作,以突出主要选择。在整个应用程序中保持动作类型样式的统一性有助于我们引导用户通过他们将要做出的选择。

列表 8.5 调用行动样式

button.primary,               ①
.button.primary {             ①
  border-color: #2c6c69;      ①
  background: #2c6c69;        ①
  color: #ffffff;             ①
}                             ①

button.secondary,             ②
.button.secondary {           ②
  border-color: #2c6c69;      ②
  color: #2c6c69;             ②
}                             ②

① 适用于继续结账链接

② 适用于继续购物链接

剩下要样式化的就是表格中购物项目中的移除按钮。这个按钮被赋予了 destructive 类。至于前两种按钮类型,我们想要更改边框、文本和轮廓颜色,这次改为红色而不是青色,以强调这个动作是破坏性的。我们通过给 border-radius 赋值为 50% 使按钮看起来是圆形的。我们还减少了 padding 值;否则,移除按钮将成为我们表格中最突出的元素,这是不希望的。最后,我们通过 vertical-align 属性在按钮中间居中图像。这个属性可以应用于内联和内联块级元素,它决定了元素根据其周围的内联和内联块元素如何垂直对齐。我们想要在按钮内垂直居中图像,所以我们将使用属性值 middle

列表 8.6 展示了移除按钮的 CSS。图 8.7 展示了每个状态下的输出。

列表 8.6 移除按钮

button.destructive {
  border-color: #9d1616;
  color: #9d1616;
  border-radius: 50%;
  padding: 5px;
}

button.destructive img {     ①
  vertical-align: middle;    ①
}                            ①

button.destructive:hover,
button.destructive:focus {
  outline-color: #9d1616;
}

① 在按钮内居中图像

图片

图 8.7 链接和按钮样式

8.2.3 输入字段

我们将对输入字段进行一些最小限度的样式化。我们不会处理无效输入或错误信息的样式,因为本章的重点是创建包含表格的响应式布局。有关样式化表单的详细信息,请参阅第十章。

对于这个布局,我们将给我们的输入字段与按钮和链接相同的基样式。然而,我们不会编写新的规则,而是将 input 选择器添加到现有的规则集中,如下所示。

列表 8.7 向基本按钮样式添加 input

button,
.button,
input {
  background: none;
  border-radius: 4px;
  padding: 10px;
  border: solid 1px #ddd;
  text-decoration: none;
}

图 8.8 显示了样式化的输入字段。

图片

图 8.8 格式化字段

8.2.4 表格

接下来,我们将样式化表格。我们将只关注与主题相关的样式,例如颜色和边框。我们将处理布局和响应性,从第 8.3 节到第 8.5 节。

我们将表格分为三个部分,我们将按顺序处理:

  • 头部<thead>

  • 主体<tbody>

  • 尾部<tfoot>

样式化表头

我们将首先样式化我们的表头。因为表头不如表格本身的内容重要,所以我们将给它们一个比其他文本略小的字体大小和较浅的颜色。我们还将降低它们的默认 font-weightboldnormal。通过稍微减弱表头,我们在表格中创建了一个视觉层次,并强调了用户最关心的内容(他们购物车中的项目)。规则如下所示。

列表 8.8 样式化单元格内容

th {
  color: #3a3a3a;
  font-weight: normal;
  font-size: .875em;
}

到目前为止,我们的表头看起来像图 8.9 所示。

图片

图 8.9 样式化表头单元格

在第二个单元格中加粗项目

在表格体(<tbody>)中,我们将通过使文本粗体来强调项目名称(在第二列)。为了给项目添加font-weight属性并设置其值为bold,我们将使用伪类:nth-of-type(),它允许我们根据同一标签的兄弟元素中的位置选择元素。为了定位表格体的每一行的第二个单元格——第二个<td>元素——我们使用tbody td:nth-of-type(2)。列表 8.9 显示了我们的规则。

列表 8.9 在表格体的每一行中粗体显示第二个单元格

tbody td:nth-of-type(2) {
  font-weight: bold;
}

图 8.10 显示了我们的更新后的表格,其中项目名称被粗体显示。

图 8.10 粗体显示的项目名称

条纹化行

接下来,我们将条纹化表格的行。我们再次使用:nth-of-type(),但这次我们使用关键字even。以下列表中的规则选择了表格体(<tbody>)中的偶数行,我们给这些行添加了浅青色的背景颜色。

列表 8.10 条纹化表格体的行

tbody tr:nth-of-type(even) {
  background: #f2fcfc;
}

图 8.11 展示了我们的更新后的行。

图 8.11 条纹化行

在页脚中粗体显示总金额

我们想要粗体显示总金额,它出现在表格的页脚单元格中。因为我们已经有了一个粗体显示文本的规则——我们创建的用于粗体显示项目名称的规则——我们可以将tfoot td选择器添加到现有的规则中,如下面的列表所示。

列表 8.11 粗体显示页脚

tbody td:nth-of-type(2),
tfoot td {
  font-weight: bold;
}

我们更新的页脚看起来像图 8.12。

图 8.12 粗体显示的总金额

处理边框

我们将在所有行上添加顶部边框,无论它们在表格中的位置如何。我们还想移除单元格之间出现的突出白色线条。如果我们加深行的背景颜色,它就变得特别明显(图 8.13)。

图 8.13 单元格之间的白色线条

让我们从移除单元格之间的间距开始。但首先,为什么会有这些白色线条呢?如果我们决定给表格中的每个单元格添加边框,我们的表格就会看起来像图 8.14。

图 8.14 单个单元格的边框

注意,每个单元格周围都有一个方块。我们看到的行间距是单个单元格之间的间距。如果我们折叠边框,使得单元格之间只出现一条线,那么间距就会消失(图 8.15)。

图 8.15 带有折叠边框的表格

我们用来移除间距并合并边框的 CSS 属性是border-collapse,其值为collapse。添加了这个属性后,我们也可以给我们的行添加边框。在我们折叠边框之前,只有单个单元格可以添加边框。因此,在我们的项目中,我们在表格上折叠边框,并在每一行的顶部应用边框,如下面的列表所示。

列表 8.12 处理表格的边框

table { border-collapse: collapse; }
tr { border-top: solid 1px #aeb7b7; }

图 8.16 展示了我们的更新后的表格。

图 8.16 样式化表格边框

接下来,让我们继续到项目摘要部分内的描述列表。

8.2.5 描述列表

接下来,我们还需要为主题化摘要部分中的描述列表(<dl>)进行样式化。描述列表通常用于创建术语表或显示元数据,对于包含项目和它们值的摘要来说非常合适。我们将以与我们的表格标题相同的方式样式化描述术语(<dt>)。我们希望从描述本身(<dd>)中淡化它们,其中包含每个元素的美元价值。因为我们想以与表格标题相同的方式样式化它们,所以我们将添加dt作为选择器到现有的规则中,类似于我们在 8.2.3 节中添加输入到按钮规则中所做的。

之后,我们将使用:after伪元素和content属性在每个<dt>后添加两个冒号。CSS 和输出在列表 8.13 和图 8.17 中显示。

列表 8.13 设置描述列表的样式

th, dt {             ①
  color: #3a3a3a;
  font-weight: normal;
  font-size: .875em;
}

dt::after {          ②
  content: ": ";     ②
}                    ②

① 为我们现有的标题样式添加描述术语

② 在每个描述术语后添加冒号

图片

图 8.17 描述列表主题

8.2.6 卡片

为了给布局增加深度并在部分之间实现分离,我们将把我们的部分容器样式化为卡片。卡片是一种常用的设计模式,通过将内容封装在类似扑克的盒子或容器中来分离内容。这个概念与我们在第六章中创建个人资料卡片时使用的概念相同。

为了实现我们的卡片设计,我们将在<body>上添加浅青色背景,并用类似悬停于<body>之上的阴影来勾勒部分。为了创建阴影,我们将使用box-shadow属性,它允许我们控制 x 轴和 y 轴上添加的阴影量,以及模糊(模糊度)、它应该扩散的距离以及阴影的颜色。图 8.18 详细说明了属性值的应用。

图片

图 8.18 box-shadow属性值

可选地,我们还可以设置一个inset的值来指示阴影应该在元素内部而不是外部。为了完成我们卡片的外观,我们将使用border-radius值为4px来使角落弯曲——这个值与我们用于链接、按钮和输入的值相同。下面的列表显示了我们的部分规则。

列表 8.14 设置部分的样式

body {
  font-family: 'Raleway', sans-serif;
  color: #171717;
  font-variant-numeric: lining-nums;
  background: #fbffff;                ①
}

section {                             ②
  background: #ffffff;                ②
  border-radius: 4px;                 ②
  box-shadow: 2px 2px 7px #aeb7b7;    ②
}                                     ②

① 为页面添加背景颜色

② 使我们的部分看起来像卡片

图 8.19 显示了我们的样式化部分。然而,请注意,在摘要卡片底部,链接延伸到卡片之外。这种效果发生是因为链接默认的display值为inline

图片

图 8.19 主题部分溢出的链接

当向内联元素(在这种情况下是链接)添加垂直内边距时,元素的高度在页面流中不会增加。因此,它只占据其内容(文本)的空间,这就是为什么它不会增加卡片的高度。为了解决这个问题,我们将它们的 display 值从 inline 更改为 inline-block。以下列表显示了更新的规则。

列表 8.15 部分样式

button, .button, input {
  background: none;
  border-radius: 4px;
  padding: 10px;
  border: solid 1px #ddd;
  text-decoration: none;
  display: inline-block;
}

在修复到位后,我们的布局看起来像图 8.20。

图 8.20 样式卡

在处理完主题后,我们可以开始关注布局。

8.3 移动布局

我们将从移动布局开始。我们首先要做的是使我们的表格响应式。

8.3.1 表格移动视图

传统的表格布局在移动设备上效果不佳,因为表格需要大量的宽度,而手机屏幕无法提供。为了适应手机,我们将使表格的行和单元格在窄屏幕上表现得更像卡片。

使用媒体查询

我们将首先使用媒体查询,当视口宽度小于或等于 549px 时应用一组规则到表格上。查询将是 @media(max-width: 549px) { }。注意这里我们使用了 max-width。在之前的章节中,我们使用了 min-width,因为我们只想在屏幕达到一定大小时应用样式。在这种情况下,我们做的是相反的:我们希望样式在屏幕达到一定宽度之前应用。

在这个媒体查询内部,我们将定义表格在窄屏幕上应该看起来是什么样子。图 8.21 显示了我们的表格当前的样子以及我们想要达到的效果。

图 8.21 移动设备上的前后表格对比

要查看窄屏幕或移动版本,大多数浏览器的开发者工具允许我们模拟特定设备的屏幕。在 Google Chrome 中,要选择特定设备,我们通过点击 DevTools 栏顶部的带有手机的图标来切换设备工具栏,然后选择我们想要使用的设备,如图 8.22 所示。然而,值得注意的是,这种模拟是有限的,不应取代在物理设备上的测试。

图 8.22 Chrome DevTools 中的设备模拟

改变表格的显示结构

首先,我们将所有内容垂直堆叠,而不是将每行的元素水平表示。我们通过给我们的行和单元格设置 display 值为 block 来完成这个任务。默认情况下,表格单元格的 display 值为 table-cell,而行有 display 值为 table-row

接下来,我们将图片浮动到左侧(第七章),这样行的其余内容就可以围绕它包裹。我们还为图片周围添加了一些边距,以在图片和行的其余内容之间创建一些空白。以下列表显示了我们的媒体查询和更新的单元格样式。

列表 8.16 移动单元格和行布局

@media(max-width: 549px) {
  td, tr { display: block; }
  table td > img {             ①
    float: left;
    margin-right: 10px;
  }
}

① 专门针对单元格的直接子图像,以避免在按钮(红色 X)中浮动图像

我们正越来越接近我们的目标,但我们的表头信息并不在我们需要的位置。如图 8.23 所示,表头信息位于表格顶部,而不是在表格体行中的每条信息之前。

从数据属性显示内容

要将表头信息放置在每个内容之前,我们不会使用表头。相反,我们将在 HTML 中的单元格添加一些数据属性:<td data-name="Item">Red Grapes, 1lb</td>。这些数据将驱动对每一行的标签,而不是表头中的内容。

图 8.23 表头位于移动表格的顶部。

我们通过使用绝对定位将表头移出屏幕,如图 8.17 列表所示。我们不想使用display:none,因为表头中的信息仍然需要辅助技术。通过绝对定位将其移出屏幕(使用大负值),我们视觉上隐藏了它,但不是程序上。

列表 8.17 隐藏表头

@media(max-width: 549px) {
  ...
  thead {
    position: absolute;
    left: -9999rem;
  }
}

当我们的表头移除后(如图 8.24 所示),我们可以专注于从data-name属性中提取数据并将其显示给用户。我们注意到在移除表头后,内容有所偏移,因为我们的表格目前没有占据整个屏幕宽度。我们将在本节稍后解决这个问题。现在,让我们完成对表头信息的处理。

图 8.24 从视图中移除表头

要显示属性值,我们使用attr()函数,它接受一个属性名并返回一个值。在我们的用例中,我们的content属性将是td[data-name]: before { content: attr(data-name) ":"; }。图 8.25 详细说明了这一点。

图 8.25 在单元格前添加表头信息

为了对齐我们的标签和内容,我们使用text-alignfloat的组合。我们在单元格中使用text-align: right来右对齐单元格内容——项目名称、单价、输入字段、总计和按钮——然后将标签(从data-name属性获取的内容)浮动到左侧以在两个元素之间创建一个间隔,如图 8.26 所示。我们还为单元格添加了一些填充,以增加内容行之间的空白。列表 8.18 显示了用于对齐表格单元格内容的 CSS。

列表 8.18 显示data-name属性的值

@media(max-width: 549px) {
  ...
  td {
    text-align: right;
    padding: 5px;
  }
  td[data-name]::before {
    content: attr(data-name) ":";
    float: left;
  }
}

图 8.26 对齐标签和内容

现在数据在data-name属性中已被显示,让我们将其样式调整为与定义标题相匹配。我们不必复制样式,可以将选择器附加到现有规则中,如下列所示。

列表 8.19 完成细节

@media(max-width: 549px) {
  ...
  th, dt, td[data-name]::before {
    color: #3a3a3a;
    font-weight: normal;
    font-size: .875em;
  }
}

全宽

标签样式完成后,让我们将注意力转回到我们的表格并没有占据其可用的全部宽度这个问题上。我们可以通过使用规则 table { width: 100%; } 来将其宽度设置为 100% 来解决这个问题。因为我们希望表格无论屏幕大小如何都能占据其全部宽度,所以我们将在媒体查询之外添加此规则。

我们几乎完成了表格的移动样式(图 8.27)。剩下要做的就是处理表格页脚。

图 8.27 宽度全表的表格

表格页脚

在表格页脚(<tfoot>)中,我们希望将文本对齐到单行。为此任务,我们将使用 Flexbox,并设置 justify-content 属性值为 space-betweenalign-items 属性值为 baseline,以在行的两端对齐标签和总计。(要了解 CSS Flexbox 布局模块的工作原理,请参阅第六章。)

查看我们的表格页脚 HTML(列表 8.20),我们注意到我们的第一个单元格是一个表格标题(<th>),而不是表格数据单元格(<td>),这是有道理的,因为它描述了该行的内容。

列表 8.20 表格页脚 HTML

@media(max-width: 549px) {
  <tfoot>
    <tr>
      <th colspan="4" scope="row">Total:</th>
      <td id="total">
        <!-- value calculated & inserted by JS -->
      </td>
    </tr>
  </tfoot>
}

如果我们仔细观察图 8.27,我们会注意到页脚内容没有任何填充;它直接紧挨着卡片和行边框。之前,我们为所有表格数据单元格添加了填充,而不是标题,所以现在我们将为页脚添加填充。以下列表显示了我们对创建我们的移动表格布局所编辑和创建的样式回顾,以及我们对表格页脚的更改。

列表 8.21 移动表格 CSS

th, td, td[data-name]::before {
  color: #3a3a3a;
  font-weight: normal;
  font-size: .875em;
}
@media(max-width: 549px) {
  td, tr { display: block }
  table td > img {
    float: left;
    margin-right: 10px;
  }
  thead {
    position: absolute;
    left: -9999rem;
  }
  td {
    text-align: right;
    padding: 5px;
    vertical-align: baseline;
  }
  td[data-name]::before {
    content: attr(data-name) ":";
    float: left;
  }

  tfoot tr {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
  }
  tfoot th { padding: 5px }
}
  table { width: 100% }

图 8.28 显示了完成的表格。

图 8.28 窄屏幕上显示为卡片的表格

现在表格在移动设备上看起来很好,我们将把注意力转向描述列表和整体布局。与创建特定于小屏幕样式的规则不同,下一组规则将适用于任何屏幕宽度,因此它们不会在媒体查询内。我们将首先解决描述列表(<dl>)。

8.3.2 描述列表

与表格不同,表格在移动和桌面屏幕上看起来完全不同,描述列表将不受屏幕宽度的影响而保持相同的外观。在更宽的屏幕上,其位置会改变,但列表本身不会变。因为描述列表不受屏幕大小的影响,所以我们将布局样式放在媒体查询之外。

为了显示项目描述列表,我们将使用 grid(第二章)。我们将定义两列,并让项目在两列中自动定位。如果没有给出特定的放置指令,网格容器的子元素将放置在第一个可用的空间中,这正是我们要利用的行为。我们还将定义一个间隙,并在容器中添加一些填充,以在网格和卡片中分隔元素。最后,我们将左对齐数字。列表 8.22 展示了 CSS,图 8.29 展示了项目描述列表的前后布局。

列表 8.22 项目描述列表样式

dl {
  display: grid;
  gap: .5rem;
  grid-template-columns: auto max-content;     ①
  padding: 0 1rem;
}
dd { text-align: right; }

① 我们为第二列使用 max-content,因为我们不希望数字换行,这会使它们难以阅读。

图片

图 8.29 项目描述列表前后布局对比

8.3.3 动作链接

我们的项目描述列表看起来好多了,但动作链接仍然需要一些帮助。正如我们在 8.3.2 节中为项目描述列表所做的,我们希望无论屏幕大小如何,动作链接的布局都相同,因此样式将放在我们的媒体查询之外。

首先,我们将为链接的容器添加一些填充,并使用 text-align 属性来居中它们。当没有足够的空间让链接并排显示并且它们堆叠时,我们将为它们添加一些边距以防止它们紧挨在一起。列表 8.23 展示了代码。图 8.30 展示了输出前后的版本。

列表 8.23 动作链接

.actions {
  padding: 1rem;
  text-align: center;
}
.actions a {
  margin: 0 .25rem .5rem;
}

图片

图 8.30 动作链接前后布局对比

8.3.4 填充、边距和边距合并

我们部分内的所有内容(除了标题)都是为移动设备布局的。浏览器默认给标题添加边距,但这个设置并没有达到我们想要的效果;它不是在卡片边缘和标题之间创建垂直空间,而是将卡片向下推。边距会推动内容,但不会影响元素或其内容占据的空间量,这就是为什么顶部的边距(标题)会从卡片中溢出。

如果我们移除标题的边距并为其添加填充,卡片将扩展,但两张卡片之间的间隙将消失。因此,我们需要给 section 本身添加一些边距以在两张卡片之间添加空间。如果我们给部分添加一个值为 1remmargin(1-rem 顶部和底部,但不是左右),我们仍然会在两张卡片之间保持 1-rem 的间隙——这是边距合并的直接结果。除非通过浮动或弹性改变了元素的定位,否则相邻的两个边距将合并为两个边距中较大的那个。图 8.31 图解了这一效果。

图片

图 8.31 边距和边距合并的效果

要在卡片边缘和标题之间添加空间,我们将用填充替换卡片标题的边距。然后,我们将向卡片添加部分边距以恢复丢失的垂直空间。最后,我们将向主体添加填充,以便卡片不会紧贴屏幕的左右边缘。以下列表显示了如何操作。

列表 8.24 部分边距和标题填充

body {
  font-family: 'Raleway', sans-serif;
  color: #171717;
  font-variant-numeric: lining-nums;
  background: #fbffff;
  padding: 1rem;
}

section { margin: 1rem 0 }

section h2 {
  padding: 1rem;
  margin: 0;
}

移动布局完成(图 8.32)后,让我们增加平板电脑和笔记本电脑的屏幕宽度。

图 8.32 卡片标题前后对比

8.4 中等屏幕布局

我们为移动设备所做的几乎所有事情在中等尺寸的屏幕上看起来都会很好。因为我们使用媒体查询将表格布局更改限制在宽度小于或等于 549 像素的屏幕上,所以我们编写的用于编辑表格的样式不会应用于任何宽度为 550 像素或更宽的屏幕。图 8.33 显示了视口宽度为 549 像素和 550 像素时的表格。在 550 像素的宽度时,我们回到了标准的表格布局。

图 8.33 表格断点

8.4.1 右对齐数字

接下来,我们将更新表格中值的对齐方式。由于这使直观计算更容易,通常将数字右对齐,特别是如果它们在列中总计。我们将更新单价、数量和总计的标题和单元格,使它们右对齐。

要选择标题和单元格,我们可以使用 :nth-of-type(n) 选择器。要选择单价列(第三列)的标题和单元格,我们将使用 th:nth-of-type(3), td:nth-of-type(3) { ... } 并对其他所有列(数量、总计和操作)重复相同的过程。

我们也可以稍微改变一下思考过程。我们想要在第一列之后的所有列中右对齐。在 :nth-of-type() 中,我们不仅可以传递数字,还可以传递模式。在 8.2.4 节中,我们使用这个技巧通过传递参数 even 来设置我们行的背景颜色。在这种情况下,我们将传递一个自定义模式,使用参数 n+3。这个模式表示我们想要从 n 是迭代器且 3 是起始点开始选择所有匹配的元素。图 8.34 说明了这个模式。

图 8.34 nth-of-type(n+3) 解释

使用这种技术,我们可以选择每行的第三、第四、第五和第六个单元格,并将它们的内文右对齐,如列表 8.25 所示。请注意,我们将我们的规则放在一个具有 min-width550px 的媒体查询中。我们不希望将这些更改应用于较小的屏幕(根据我们之前的媒体查询定义为任何小于或等于 549 像素的屏幕),因此我们使用第二个媒体查询仅将这些样式应用于宽度为 550 像素或更宽的屏幕。

列表 8.25 右对齐内容

@media (min-width: 550px) {
  th:nth-of-type(n+3),
  td:nth-of-type(n+3) {
    text-align: right;
  }
}

在我们的样式应用后(图 8.35),我们注意到一些事情:

  • 我们的前两列需要它们的标题左对齐以匹配其内容。

  • 字段内的数字没有自动右对齐。

  • 删除按钮紧挨着卡片边缘。

图片

图 8.35 右对齐的数字和操作列

让我们按顺序解决这些问题。

8.4.2 左对齐前两列

我们将利用具体性来处理标题。因为,作为一个选择器,thth:nth-of-type(n+3) 更不具体,我们可以创建一个将文本左对齐并保留其他列的先前规则的 th 规则。th 规则将使所有列的标题内容左对齐。然后,我们将覆盖 th:nth-of-type(n+3) 规则中数字和按钮列的 text-align 属性值。下面的列表显示了所做的更改。

列表 8.26 更新表头规则

@media (min-width: 550px) {

  th { text-align: left }

  th:nth-of-type(n+3),
  td:nth-of-type(n+3) {
    text-align: right;
  }
}

现在我们的前两个表头左对齐而不是居中(浏览器的默认设置),我们的其他列保持右对齐(图 8.36)。

图片

图 8.36 样式化标题

8.4.3 在输入字段中右对齐数字

我们可以选择仅在表格视图中或无论屏幕大小如何都始终右对齐输入字段内的文本,我们这样做是在媒体查询之外。因为我们也在移动视图中将数字和总计右对齐,所以更新输入字段样式以适用于所有显示尺寸并包含在主题中似乎是合理的。

要选择数字类型的输入,我们可以使用属性选择器:input[type= "number"] { ... }。我们将把 input[type="number"] { text-align: right } 添加到样式表中 外部 媒体查询,因为我们希望无论屏幕大小如何都应用它。

在输入字段内的文本对齐(图 8.37)后,我们需要解决的最后一部分是我们所有表格数据单元格和表头的填充。

图片

图 8.37 输入字段中的右对齐文本

8.4.4 单元格填充和边距

为了完成我们的表格(中等屏幕)视图,我们将在表头、主体和页脚的单元格中添加填充和边距。为了实现这种效果,我们在中等屏幕尺寸的媒体查询中添加了 td, th { padding: 10px }。下面的列表显示了我们所做的全部更改,以实现表格布局。

列表 8.27 中等尺寸屏幕

input[type="number"] { text-align: right }

@media (min-width: 550px) {

  th { text-align: left }

  th:nth-of-type(n+3),
  td:nth-of-type(n+3) {
    text-align: right;
  }

  td, th { padding: 10px }
}

现在我们已经为小屏幕和中屏幕尺寸进行了样式化(图 8.38),让我们更进一步,处理宽屏幕。

图片

图 8.38 完成的移动和平板布局

8.5 宽屏幕

随着我们继续增加屏幕宽度,摘要部分由于定义标题和描述之间的距离增加而变得难以阅读(图 8.39)。

图片

图 8.39 桌面视图的摘要(视口宽度 955 像素)

由于屏幕变宽时我们有更多的水平空间可以利用,因此当视口宽度达到 995 像素或更宽时,我们将摘要部分移至购物车部分旁边,如图 8.40 中的线框所示。

图片

图 8.40 布局线框

为了根据屏幕宽度为 955 像素或更宽的条件有条件地更改布局,我们将创建媒体查询 @media (min-width: 995px) {}。在以下列表中显示的 HTML 中,我们有两个部分周围有一个具有 section-container 类的 <div> 容器。

列表 8.28 页面 HTML

 <main>
    <h1>Checkout</h1>

    <div class="section-container">         ①

      <section class="my-cart">             ②
        <h2>My Cart</h2>                    ②
        <table> ... </table>                ②
      </section>                            ②

      <section class="summary">             ③
        <h2>Summary</h2>                    ③
        <dl> ... </dl>                      ③
        <div class="actions"> ... </div>    ③
      </section>                            ③

    </div>                                  ④

  </main>

① 两个卡片(购物车和摘要)的容器

② 购物车卡片

③ 摘要卡片

④ 容器结束

在我们新的媒体查询中,我们将容器设置为 display 属性值为 flex。这个值允许两个项目并排排列并在 x 轴上对齐。然后我们在两个部分之间添加一个 20px 的间隙。

Flexbox 将自动计算为每个部分分配的空间量。我们可以通过 flex-growflex-shrinkflex-basis 属性来影响浏览器分配尺寸的方式。我们将给摘要部分一个 flex-basis 值为 250px,给购物车部分一个 flex-grow 值为 1

应用到摘要卡片上,flex-basis 将设置浏览器开始计算为每个部分分配多少空间时该部分的初始大小。如果应用 flex 的内容可以容纳 250 像宽的部分,浏览器不会更改该部分的尺寸;否则,浏览器将根据需要调整该部分。flex-grow 属性告诉浏览器,如果在 flex 应用到内容后还有剩余空间,则应使该元素变宽以使用额外的空间。图 8.41 显示了这两个属性如何影响元素的大小。

图片

图 8.41 影响 flex 应用的元素大小

通过使用 flex-growflex-basis,我们可以控制表格相对于摘要卡片的宽度。因此,我们在以下列表中使用媒体查询来处理我们的项目。

列表 8.29 在宽屏幕上并排放置两个卡片

@media (min-width: 955px) {
  .section-container {
    display: flex;
    gap: 20px;
  }
  section.my-cart { flex-grow: 1; }
  section.summary { flex-basis: 250px }
}

图 8.41 显示了屏幕宽度为 955 像素时的布局。但是,如果我们使屏幕更宽,例如用于额外的宽曲显示屏,我们最终会到达一个点,内容再次变得难以阅读(图 8.42)。因为我们为摘要卡片设置了 flex-basis 值,所以它仍然可读,但由于表格是通过 flex-grow 属性来保持增长的,所以它变得难以操作。

图片

图 8.42 2,000 像素宽屏幕上的布局

为了防止这种增长,我们可以限制 <main> 元素的宽度(它包含我们的主要标题和卡片)。这种更改确保无论用户的显示有多宽,或者用户如何选择扩展窗口,内容都保持可用。我们可以通过给左右边距赋值为 auto 来居中文本,如下所示。

列表 8.30 main 元素的最大宽度

main {
  max-width: 1280px;
  margin: 0 auto;
}

如果我们再次查看我们的布局,在应用了这些最后样式的一个极其宽的屏幕上(如图 8.43),我们会看到我们已经限制了内容并使其居中。

图片

图 8.43 限制宽度的布局

经过这些最后的编辑,我们的项目就完成了。从一个 HTML 文件中,我们根据屏幕宽度创建了三个不同的布局(如图 8.44)。

图片

图 8.44 三个屏幕尺寸的最终输出

摘要

  • 数字样式可以通过 font-variant-numeric 属性来控制。

  • 媒体查询允许我们根据屏幕大小有条件地应用样式。

  • 根据它们所修饰的内容或代表的内容来命名 CSS 类有助于创建易于理解和维护的名称。

  • 可以使用 HTML 属性值来选择元素。

  • 可以通过 CSS 使用伪元素、content 属性和 attr() 函数来显示 HTML 属性值。

  • 边距可以合并。

  • 设置为 display:flex 的元素可以通过 flex-growflex-shrinkflex-basis 来控制。

  • :nth-of-type 可以接受数字、关键词或自定义模式来根据元素相对于容器内相同类型元素的位置来定位元素。

9 创建虚拟信用卡

本章涵盖

  • 使用 Flexbox 和position进行布局

  • 使用背景图像和尺寸

  • 加载和应用本地字体

  • 使用过渡和backface-visibility属性创建 3D 效果

  • 使用text-shadowborder-radius属性等额外样式

正如我们在第三章中看到的,CSS 中的动画为创建交互式网络体验提供了许多机会。在第三章中,我们使用动画给用户一种感觉,在他们等待任务完成时,背景中似乎有事情发生。现在我们将使用动画来响应用户的交互并为信用卡图像创建翻转效果。在一侧,动画将显示信用卡的正面;在悬停或点击移动设备时,它将翻转以显示信用卡的背面。

这种效果对用户很有用,因为我们正在重新创建他们的信用卡可能的样子,显示他们在网上购买东西时需要从卡片中输入哪些信息,例如有效期或安全码。动画是通过在网络上重新创建它来表示现实生活中的某种东西的一种方式。这个项目与第八章中的项目相辅相成,在第八章中我们设计了一个结账购物车。

我们还将探索样式图像以设置信用卡的背景和卡片上的图标。我们将使用 CSS Flexbox 布局模块进行布局,以及如阴影、颜色和边框半径等样式属性。到本章结束时,我们的布局将看起来像图 9.1。

图 9.1 信用卡正面和背面的最终输出

在我们进行项目的过程中,请随意尝试自定义它以匹配您的风格。例如,尝试不同的背景图片或字体。这个项目是一个很好的机会来调整样式以适应您的风格。让我们开始吧。

9.1 开始

我们的 HTML 由两个主要部分组成。在表示虚拟卡的总体部分中,有一个正面和一个背面。您可以在 GitHub 仓库的chapter-09文件夹中找到起始 HTML(mng.bz/Bm5g),在 CodePen(codepen.io/michaelgearon/pen/YzZKMKN)以及以下列表中找到。

列表 9.1 项目 HTML

<section class="card-item">                                                ①
  <section class="card-item__side front">                                  ②
    <div class="card-item__wrapper">
      <div class="card-item__top">                                         ③
        <img src="chip.svg" class="card-item__chip" alt="card chip">       ③
        <div class="card-item__type">                                      ③
          <img src="logo.svg" alt="Card Type" class="card-item__typeImg"   ③
➥ height="37" width="152">                                                ③
        </div>                                                             ③
      </div>
      <div class="card-item__number">                                      ④
        <div>1111</div>
        <div>2222</div>
        <div>3333</div>
       <div>4444</div>
      </div>
      <div class="card-item__content">                                     ⑤
        <div class="card-item__info">
          <div class="card-item__holder">Card Holder</div>
          <div class="card-item__name">John Smith</div>
        </div>
        <div class="card-item__date">
          <div class="card-item__dateTitle">Expires</div>
          <div class="card-item__dateItem">02/22</div>
        </div>
      </div>
    </div>
  </section>
  <section class="card-item__side back">
    <div class="card-item__band"></div>
    <div class="card-item__cvv">
      <div class="card-item__cvvTitle">CVV</div>
      <div class="card-item__cvvBand">999</div>
      <div class="card-item__type">
        <img src="card-type.svg" class="card-item__typeImg"
➥ height="30" width="50">
      </div>       
    </div>        
  </section>
</section>

① 整个信用卡的容器

② 卡片正面的容器

③ 显示卡片顶部正面部分的区域

④ 显示卡片号码的卡片中间正面部分

⑤ 显示有效期和持卡人姓名的卡片底部正面部分

我们还有一些初始的 CSS 来将背景颜色改为浅蓝色并增加页面顶部的边距,如下所示。

列表 9.2 起始 CSS

* {
  box-sizing: border-box;
}
body {
  background: rgb(221 238 252);
  margin-top: 80px;
}

我们正在使用我们在第一章中查看的通用选择器来设置所有 HTML 元素的box-sizing值为border-box。此选择器有两个值:

  • content-box——这是计算元素宽度和高度的默认值。如果content-box的高度和宽度是250px,任何边框或填充都将添加到最终渲染的宽度中。例如,如果周围有2px的边框,最终渲染的宽度将是254px

  • border-box——这个值与border-box的区别在于,如果我们将元素高度设置为250px,任何边框和填充都将包含在这个指定的值中。content-box会随着填充和边框的增加而减少。

图片

图 9.2 box-sizing对元素尺寸的影响

图 9.2 展示了示例。我们的起点看起来像图 9.3。

图片

图 9.3 起点

9.2 创建布局

前面和背面都有一个类名为card-item__side。前面还有一个类名为front的二次分配,背面有一个名为back的二次类。有两个类名——一个在两边都相同,另一个不同——这允许我们使用.card-item__side选择器(它们共有的类)分配适用于两边的样式,并在.front {}.back {}的单独规则中分配仅适用于一侧的样式。

让我们从将卡片在屏幕上居中开始。第一步是设置卡片的最大宽度为430px和固定高度为270px。我们还将其位置设置为relative,这在我们在本章后面(9.5 节)将卡片背面放在前面以创建翻转效果时将很有用。

最后一步是设置卡片的左右边距为auto以在浏览器窗口中水平居中卡片。为此,我们使用.card-item选择器创建以下列表中所示的规则。

列表 9.3 容器样式

.card-item {
  max-width: 430px;
  height: 270px;
  margin: auto;
  position: relative;
}

图 9.4 显示了更新的定位。

图片

图 9.4 居中的信用卡

9.2.1 设置卡片大小

现在我们已经为卡片设置了最大宽度和高度,我们想要确保前面和背面填充它们在父容器(卡片)内可用的整个空间。因此,我们将使用类选择器.card-item__side将卡片两面的高度和宽度都设置为100%,如下列所示。

列表 9.4 前面和背面共享的容器

.card-item__side {
  height: 100%;
  width: 100%;
}

添加了这段代码后,我们的卡片面(前面和背面)扩展以匹配其父容器的尺寸,如图 9.5 所示。

图片

图 9.5 卡片面(前面和背面)与父容器尺寸匹配。

9.2.2 样式化卡片的前面

对于卡片的前面,我们分为三个主要部分(图 9.6):

  • 卡片的顶部有两张图片,一张显示芯片,另一张显示信用卡类型(如 Visa 或 MasterCard)。

  • 中间是卡号,它均匀分布在卡片的宽度上。

  • 在底部是持卡人的姓名和卡片的过期日期。这些元素位于相反的端点。

图 9.6 卡片前面的线框图

在我们开始为卡片前面的各个部分添加样式之前,让我们给卡片面添加一些填充,这样内容就不会紧贴边缘。我们将给它们留出一些空间。下面的列表显示了代码。

列表 9.5 卡片前面的容器样式

.front {
  padding: 25px 15px;
}

记住,在项目最初提供的样式表中,我们将所有元素的box-sizing设置为border-box。随着填充的增加,我们看到改变box-sizing并没有增加卡片面<section>的尺寸;相反,它减少了内容可用的空间(图 9.7)。

图 9.7 带有添加填充和盒模型图的卡片

卡片的顶部

我们正在使用 Flexbox 来布局卡片。正如我们所学的,Flexbox 很可能是放置单轴布局中项目的最佳选择。此外,我们需要利用 Flexbox 提供的额外功能,即间距和对齐功能——这是 float 所不具备的。

注意:有关 CSS Flexbox 布局模块及其相关属性详情,请参阅第六章。第七章涵盖了 float。

考虑到这些事实,我们将卡片的顶部设置为具有display属性值为flex,并设置对齐方式,使元素的顶部对齐。align-items的默认属性是stretch,它增加了flex项目的高度,使它们的高度与集合中最高的元素相匹配。

尽管我们不希望这种扭曲,但我们希望元素垂直对齐到项目的顶部。因此,我们将align-items属性设置为flex-start。然后我们将justify-content属性设置为space-between,这样就可以在轴上均匀分布元素,在两个元素之间创建一个间隙,并将它们放置在卡片的极端边缘。

我们将给顶部添加一些边距和填充,以便将它们相对于卡片边缘进一步定位。然后我们将芯片的宽度增加到60px。因为这张图片是 SVG 格式的,我们可以增加其大小而不影响其质量。因为我们只操作了宽度,并没有改变默认的高度,所以图片的高度将默认按比例缩放。下面的列表显示了用于样式化卡片顶部部分的规则。

列表 9.6 卡片顶部前面的布局

.card-item__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 40px;
  padding: 0 10px;
}
.card-item__chip {
  width: 60px;
}

我们更新的卡片看起来像图 9.8。

图 9.8 卡片样式化顶部部分

卡片的中间部分

在卡片正面中部,我们找到了卡片号码。同样,我们使用display属性值为flexjustify-content: space-between将数字组均匀分布在卡片的宽度上。我们还添加了填充和边距,以在数字及其周围元素之间添加空间,如以下列表所示。

列表 9.7 卡片正面中部的布局

.card-item__number {
  display: flex;
  justify-content: space-between;
  padding: 10px 15px;
  margin-bottom: 35px;
}

图 9.9 显示了我们的数字组均匀分布在卡片宽度上。

图片

图 9.9 均匀分布的数字

卡片底部

在卡片正面底部,我们有两个元素:持卡人姓名和卡片到期日期。就像我们在卡片顶部和中部所做的那样,我们想要将信息片段分开,并将它们放置在卡片的相对边缘。

我们将遵循使用 Flexbox、justify-contentpadding来放置元素的相同模式。这次我们不需要任何边距。以下列表显示了我们将使用的规则。

列表 9.8 卡片正面底部的布局

.card-item__content {
  display: flex;
  justify-content: space-between;
  padding: 0 15px;
}

图 9.10 显示了更新后的布局。接下来,我们将定位卡片背面的元素。

图片

图 9.10 卡片正面的布局

9.2.3 卡片背面的布局

背部的布局包括安全码数字和半透明带(磁条),如图 9.11 所示。让我们从半透明的背面条开始。

图片

图 9.11 卡片背面的线框图

半透明条

该条具有card-item__band类。我们希望将其高度设置为50px,并将其定位在卡片顶部30px处。我们将使用height属性来指示它应该有多高。即使<div>是空的,它也会自动占据它可用的全部宽度,因为<div>是块级元素。

为了将条向下移动而不是保持在卡片背面的顶部,我们将在卡片本身的后部添加一些填充。我们不能给它设置边距,因为它会推到之前存在的内 容(在顶部卡片中)而不是背部的顶部边缘。

虽然我们将在本章后面部分管理大多数主题,但现在让我们添加背景颜色,以便我们可以看到我们在做什么(列表 9.9)。背景是 80%不透明的深蓝色,这将允许我们放置在卡片上的部分背景图像显示出来。

列表 9.9 定位条

.back { padding-top: 30px }
.card-item__band {
  height: 50px;
  background: rgb(0 0 19 / 0.8);
}

现在我们的条看起来就像图 9.12 所示。

图片

图 9.12 卡片背面样式化的条

安全码

安全码上方有字母CVV,以及一个包含安全码的白色带(通常用于用户的签名)。字母和数字都右对齐,并嵌套在类名为card-item__cvv<div>中。

对于 CVV 字母,因为我们不需要将元素分布到卡片的宽度上,所以我们不需要使用 Flexbox。通过使用 text-align 属性将文本右对齐就足以完成任务。但我们将使用 Flexbox 在包含安全数字的白色带上,这不仅是因为它需要将文本右对齐,而且因为它使得在带内垂直对齐内容变得更加容易。让我们首先给 card-item__CVV 容器添加一些基本样式:padding 用于分隔元素,以及 text-align 属性,以便我们的文本将放置在卡片的右侧,如下面的列表所示。

列表 9.10 定位文本

.card-item__cvv {
  text-align: right;
  padding: 15px;
}

在处理完容器(图 9.13)后,我们可以单独设置字母和安全代码的样式。

图 9.13 对齐文本

对于 CVV 字母,我们只需要给这段文本添加一些边距和填充,以将其从右边缘和下面的数字偏移。因为我们希望数字在特定高度的白色带上,我们将使用 height 属性,其值为 45px。为了在盒子的中间垂直对齐文本,而不是根据文本大小计算所需的垂直填充量,我们将使用 Flexbox,并设置 align-items 属性的值为 center。我们仍然会使用填充来将文本与盒子的右边缘分开。

由于 Flexbox 的默认 justify-content 属性值为 flex-start(这将重新定位我们的文本到盒子的右侧),我们需要显式地分配给它一个值为 flex-end 的值,以便容器内的元素(文本)保持在右侧。下面的列表显示了用于样式化 CVV 和安全代码的 CSS。

列表 9.11 卡片背面的布局

.card-item__cvvTitle {
  padding-right: 10px;
  margin-bottom: 5px;
}
.card-item__cvvBand {
  height: 45px;
  margin-bottom: 30px;
  padding-right: 10px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  Background: rgb(255, 255, 255);
}

到目前为止,我们的卡片看起来像图 9.14 所示。

图 9.14 卡片上的元素定位

卡片开始成形。现在我们需要将背景图像应用到正面和背面,以及颜色和字体。这些步骤将产生巨大差异,并让我们更接近最终的外观。

9.3 使用背景图像

我们的信用卡需要某种背景图像。为了添加一个,我们将使用 background-image 属性。图像可以是任何适用于网络的格式。

9.3.1 背景属性简写

当设置元素的背景时,我们可以独立设置每个相关属性(background-imagebackground-size 等)或可以使用简写 background 属性。我们将使用以下属性和值:

  • background-image: url("bg.jpeg")

  • background-size: cover

  • background-color: blue

  • background-position: left top

如果我们使用简写形式的background属性,我们的声明最终会是background: url("bg.jpeg") left top / cover blue;. 这里为了使代码更容易阅读和讨论,图像的 URL 被截断,但在我们的代码中需要使用完整的 URL 来检索图像,正如我们在本章中会多次做的那样。图 9.15 分解了属性值。

图片

图 9.15 简写background属性

注意我们使用的是background-size属性的cover值。我们使用这个设置是为了让浏览器计算出图像应该具有的最佳大小,以便覆盖整个元素,同时尽可能多地显示图像而不失真。如果图像和我们的元素没有相同的宽高比,多余的图像将被裁剪。如果我们不希望图像的任何部分被裁剪,我们可以使用contain。图 9.16 展示了使用covercontain的示例。

图片

图 9.16 background-cover示例

尽管我们使用了background-sizecover值,但我们仍然包括一个背景颜色。当提供了图像和背景颜色时,图像总是显示在颜色之上。我们可能出于多个原因想要这样做。例如,如果图像小于元素或透明,包括背景颜色将为图像提供均匀颜色的背景。它也会在图像加载时或图像加载失败时为浏览器提供显示的内容。我们不必在我们的项目中提供这个值,但有一个与页面背景颜色不同的颜色可以帮助区分卡片和页面本身,如果图像加载失败,它将是一个好的回退位置。因为我们想让卡片的正面和背面都有背景图像,我们将更新我们的.card-item__side规则,该规则影响卡片的正面和背面,如下面的列表所示。

列表 9.12 卡片正反面的背景图像

.card-item__side{
  height: 100%;
  width: 100%;
  background: url("bg.jpeg") left top / cover blue;
}

在应用了背景图像(图 9.17)之后,我们可以专注于文本的样式。

图片

图 9.17 卡片正反面的背景图像已添加

9.3.2 文本颜色

现在我们已经设置了背景图像,我们注意到文本难以阅读,因此我们将它从黑色更改为白色,通过更新我们的.card-item选择器。列表 9.13 展示了我们的更新后的.card-item规则。

颜色对比和背景图像

验证当文本与图像重叠时颜色对比是否可访问是非常困难的,需要手动测试。在许多情况下,当窗口大小调整时,内容会重新布局,文本重叠的地方图像会改变。确保对比度始终足够的一种技术是测试文本颜色与图像最亮和最暗的部分。

值得注意的是,正如在这个项目中清楚地展示的那样,图像越繁忙,实现良好可读性的难度就越大。

列表 9.13 设置容器颜色

.card-item {
  max-width: 430px;
  height: 270px;
  margin: auto;
  position: relative;
  color: white;
}

通过更新此规则,我们已经将卡片上的所有文本更改为白色(图 9.18)。然而,我们的安全代码是在白色背景上,因此我们需要更新其规则,将其文本颜色更改为较深的颜色。

图 9.18 文本颜色更改为白色

要更改文本颜色,我们将更新.card-item__cvvBand规则(列表 9.14),该规则目前为我们提供白色带并定位安全代码在其中。我们将文本颜色更改为深蓝灰色。

列表 9.14 卡片背面白色背景

.card-item__cvvBand {
  background: white;
  height: 45px;
  margin-bottom: 30px;
  padding-right: 10px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  color: rgb(26, 59, 93);
}

在我们的安全代码可见性恢复(图 9.19)后,让我们将注意力转向卡片前面的两个文本元素:持卡人到期日

图 9.19 恢复的安全代码

在信息方面,这两段文本仅用于标记与之配对的元素,因此它们的重要性不如实际名称和日期。为了在视觉上降低其重要性,我们将降低其不透明度(列表 9.15),使其轻微半透明,并降低其亮度。在第 9.4 节中,当我们处理字体排印时,我们将出于同样的原因减小其大小。

列表 9.15 标记文本的样式

.card-item__holder, .card-item__dateTitle {
  opacity: 0.7;
}

到目前为止,卡片的最终外观已经显现(图 9.20)。我们已经设计了布局、格式、图像和颜色。但我们仍需要调整字体排印并创建主要效果:悬停时的翻转。下一步是查看字体。

图 9.20 文本不透明度降低

9.4 字体排印

对于其他项目,我们使用了免费的在线资源 Google Fonts 来加载所需的字体。我们通过链接到 Google Fonts 应用程序编程接口(API),请求所需的字体,然后将属性值设置为正在使用的字体族。但在某些情况下,我们可能想自己加载字体文件,而不是依赖于 API 或内容分发网络(CDN)。

警告:像图像和其他形式的媒体一样,字体受版权保护。在使用之前,无论字体是通过 API、CDN 还是本地托管导入,都务必确保您拥有适当的许可证。如有疑问,请咨询您的法律团队!

这两种方法都有优点和缺点。没有一种是明显优于另一种的,所以选择取决于我们正在工作的项目的需求。

使用本地或自托管字体的好处包括

  • 我们不必依赖第三方。

  • 在跨浏览器支持和性能优化方面,我们有更多的控制权,这可以使字体加载时间比第三方字体更快。

缺点包括

  • 我们必须自己进行性能优化。

  • 用户不会预先缓存该字体。

使用第三方托管字体的优点包括

  • 用户可能已经在他们的设备上缓存了该字体。

  • 导入更加简单。

缺点包括

  • 我们需要额外调用以获取字体文件。

  • 关于第三方跟踪的内容存在隐私问题。

  • 该服务可以在任何时候停止字体服务。

要从我们的本地项目文件夹加载自己的字体,我们需要创建 @font {} at 规则来定义和导入我们想要使用的字体。为了理解这个 at 规则,让我们先看看字体格式。

9.4.1 @font-face

字体可以有多种文件类型。一些知名的包括

  • TrueType (TTF)—所有现代浏览器都支持;未压缩

  • 开放字体格式 (OTF)—TTF 的进化;允许使用更多字符,如小写字母和旧式数字

  • 嵌入式开放字体格式 (EOT)—微软为网页开发;仅由 Internet Explorer 支持(已过时,因为 Internet Explorer 已停止服务)

  • Web 开放字体格式 (WOFF)—为网页创建;已压缩;在字体文件中包含元数据以包含版权信息;并由万维网联盟推荐(www.w3.org/TR/WOFF2

  • Web 开放字体格式 2 (WOFF2)—WOFF 的延续;比 WOFF 压缩 30%

  • 可缩放矢量图形 (SVG)—创建用于在网页字体普及之前在 SVG 中嵌入字形信息

当你选择要使用的字体类型时,我们通常推荐使用 WOFF 或 WOFF2。

注意:我们最近才能够在不上传多个字体格式的情况下依赖 WOFF2 文件。你仍然可以在网上找到很多关于字体的过时信息。一个有用的技巧是查看信息的发布时间——越近越好。

在处理字体时,我们知道从前几章中,我们需要导入我们想要使用的每个粗细。对于本地处理字体也是同样的道理:每个变体(粗细和样式)都需要单独包含在项目中,除非我们使用可变字体。

可变字体相对较新。与每个样式在单独的文件中不同,所有排列组合都包含在单个文件中。所以如果我们想要常规、粗体和半粗体,我们只需导入一个文件而不是三个,我们不仅能够访问这三个字体粗细,还能从细体到超粗体的一切。斜体可能不在同一个文件中;在某些字体中,斜体字形与非斜体版本的不同。

对于我们的项目,我们希望加载三种字体:Open Sans 正常体,Open Sans 粗体,和 Open Sans 斜体。这些字体是同一家族中的变体。Open Sans 有静态和可变字体版本。可变版本将斜体和常规样式分开到两个单独的文件中。对于我们的非斜体需求,因为我们正在加载多个粗细,我们将使用可变版本。

然而,对于斜体,我们将只使用一个粗细:常规。对于该粗细加载可变字体版本没有意义。因为可变字体包含了跨越所有粗细范围所需的所有信息,所以它比只包含一个粗细的文件大得多(314.8 KB),而后者只有 17.8 KB。出于性能考虑,坚持使用静态版本是有意义的。

对于每个字体,我们需要创建一个单独的@font-face规则。此 at 规则定义了字体,包括字体从哪里加载,其粗细是多少,以及我们希望它如何加载。

首先,我们声明@font-face { } 规则。在大括号内,我们将定义其特性和行为,包括四个描述符:

  • font-family—当我们通过font-family属性将字体应用于元素时,我们用来引用字体的名称。

  • src—字体正在从哪里加载。此描述符接受一个逗号分隔的列表,指定从哪些位置获取字体以及期望从每个源获取的格式。浏览器将按照列表顺序,从第一个开始,直到成功获取字体。

  • font-weight—此特定字体文件代表的粗细。在可变字体的情况下,我们将包括一个范围。

  • font-display—指定字体的加载方式。我们将使用描述符值swap。字体是加载阻塞的,即浏览器会在加载其他资源之前等待字体加载完成。swap限制了字体加载阻塞的时间。如果在这个时间段结束时字体还没有加载完成,浏览器将继续加载其他资源,并在字体加载完成后应用字体。此设置允许在字体尚未可用的情况下显示内容,并允许用户与界面进行交互。

列表 9.16 显示了我们的两个规则,这些规则必须添加到样式表的顶部。此外,除了少数例外,规则不能在现有规则内部声明。例如,.myClass { @font-face { ... } } 不会工作。一个例外是@supports at 规则,我们将在下一节中对其进行扩展。

列表 9.16 声明我们的字体

@font-face {
  font-family: "Open Sans";                                       ①
  src: url("./fonts/open-sans-variable.woff2")                    ②
       format("woff2-variations");                                ②
  font-style: normal;
  font-weight: 100 800;                                           ③
  font-display: swap;
}

@font-face {
  font-family: "Open Sans";                                       ①
  src: local("Open Sans Italic"),                                 ④
       url("./fonts/open-sans-regular.woff2") format("woff2"),    ⑤
       url("./fonts/open-sans-regular.woff") format("woff");      ⑥
  font-style: italic;
  font-weight: normal;                                            ⑦
  font-display: swap;
}

① 我们将用来引用字体的名称

② 如果浏览器可以加载可变字体,从哪里获取字体

③ 字体将支持从 100 到 800 的任何字体大小。

④ 检查设备是否已本地加载了字体

⑤ 尝试加载 woff2 格式

⑥ 如果不支持 woff2,则加载 woff

⑦ 声明此文件的字体粗细为正常(与 400 相同)

在应用此代码后,用户界面没有变化;正在使用的font-family仍然是浏览器的默认字体,因为我们还没有将字体应用到任何我们的元素上。我们还想要创建一个回退,以防浏览器不支持可变字体。在我们将字体应用到我们的元素之前,让我们看看浏览器的支持情况。

9.4.2 使用 @supports 创建回退

由于变量字体相对较新,并且并非每个人都能很好地在其设备上运行更新,因此我们将包括一个回退,以防用户的浏览器不支持变量字体。为此,我们将使用@supports at 规则。此规则允许我们检查浏览器是否支持特定的属性和值,并允许我们编写只有当提供的条件满足时才应用的 CSS。

我们的功能查询将是@supports not (font-variation-settings: normal) { ... }。因为我们的查询在条件之前有not关键字,所以它包含的样式将在条件满足时应用。换句话说,如果浏览器不支持变量字体行为,我们希望加载静态版本。

在我们文件的最顶部放置的@supports at 规则内部,我们包括了我们想要包含的正常样式版本两种字重的@font-face规则(列表 9.17)。我们还创建了一个@supports (font-variation-settings: normal) { } 规则,这次没有使用not。在这种情况下,对于支持变量字体的浏览器,我们将 9.4.1 节中创建的两个规则移动过来。这样,我们只有在浏览器支持变量字体时才加载变量字体,如果浏览器不支持变量字体,则防止文件被加载。

列表 9.17 不支持变量字体的回退

@supports (font-variation-settings: normal) {        ①
  @font-face {                                       ②
    font-family: "Open Sans";                        ②
    src: url("./fonts/open-sans-variable.woff2")     ②
➥ format("woff2-variations");                       ②
    font-weight: 100 800;                            ②
    font-style: normal;                              ②
    font-display: swap;                              ②
  }                                                  ②
}

@supports not (font-variation-settings: normal) {    ③
  @font-face {                                       ④
    font-family: "Open Sans";
    src: local("Open Sans Regular"),
         local("OpenSans-Regular"),
         url("./fonts/open-sans-regular.woff2") format("woff2"),
         url("./fonts/open-sans-regular.woff") format("woff");
    font-weight: normal;
    font-display: swap;
  }

  @font-face {                                       ⑤
    font-family: "Open Sans";
    src: local("Open SansBold"),
         local("OpenSans-Bold"),
         url("./fonts/open-sans-regular.woff2") format("woff2"),
         url("./fonts/open-sans-regular.woff") format("woff");
    font-weight: bold;
    font-display: swap;
  }
}

① 当支持变量字体时应用样式

② 将我们之前创建的变量字体规则移动到 at 规则中

③ 当不支持变量字体时应用样式

④ 正常样式的规则,字体粗细常规(400)

⑤ 正常样式的规则,字体粗细加粗(700)

在添加回退后,让我们更新我们的 body 规则以将 Open Sans 应用到我们的项目中(列表 9.18)。尽管我们添加了加载字体的回退,但我们仍将在 body 规则中的font-family属性值中包含sans-serif,以防字体文件加载失败。

列表 9.18 将字体应用到我们的项目中

body {
  background: rgb(221, 238, 252);
  margin-top: 80px;
  font-family: "Open Sans", sans-serif;
}

当字体应用后,我们可以看到我们的文本已经更新为使用 Open Sans 而不是浏览器的默认字体(图 9.21)。现在我们可以编辑我们的单个元素以调整字体粗细和样式。

图片

图 9.21 Open Sans 应用到项目中

9.4.3 字体大小和排版改进

从卡片的前面开始,我们将增加数字的字体大小并使其加粗。我们将添加到现有的规则中,如下所示。

列表 9.19 加粗并增加数字的大小

.card-item__number {
  display: flex;
  justify-content: space-between;
  padding: 10px 15px;
  margin-bottom: 35px;
  font-size: 27px;
  font-weight: 700;
}

图 9.22 显示了我们的样式化数字。

图片

图 9.22 样式化数字

接下来是数字下面的文本,我们希望减小卡片持有者到期日的大小。我们将它们的font-size设置为15px,并增加名称和日期的大小和font-weight,如下所示。

列表 9.20 持卡人信息和到期日期的排版

.card-item__holder, .card-item__dateTitle {   ①
  opacity: 0.7;                               ①
  font-size: 15px;                            ①
}                                             ①

.card-item__name, .card-item__dateItem {      ②
  font-size: 18px;                            ②
  font-weight: 600;                           ②
}                                             ②

① 卡片持有者和到期日

② 姓名和到期日期

在处理完卡片正面的文本元素(图 9.23)后,让我们将注意力转向背面。

图片

图 9.23 卡片正面的排版

在背面,我们需要更新安全代码使其为斜体。我们将使用font-style: italic更新现有的规则,如下所示。

列表 9.21 使卡片安全号码倾斜

.card-item__cvvBand {
  background: white;
  height: 45px;
  margin-bottom: 30px;
  padding-right: 10px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  color: #1a3b5d;
  font-style: italic;
}

现在卡片已经设置了样式(图 9.24),我们准备应用翻转效果。

图片

图 9.24 完成的排版样式

9.5 创建翻转效果

接下来,我们将为支持hover交互的设备创建翻转效果。我们首先调整位置,使卡片背面覆盖在正面之上。然后,我们将使用backface-visibilitytransform属性来放置卡片。为了实现动画变化,我们将使用过渡效果。

9.5.1 位置

要实现翻转效果,我们通过backface-visibility属性将卡片面堆叠在一起。然后我们将切换显示哪一面。当我们使用backface-visibility属性并暴露背面时,我们在水平轴上进行旋转;因此,我们需要反转背面,使其内容镜像。想象一下拿一张描图纸并在背面画一个图像。当我们看正面时,从背面通过它出现的图像是镜像的。这正是我们在这里要实现的效果。我们用来堆叠正面和背面然后翻转的 CSS 在列表 9.22 中。我们将代码放在一个媒体查询中,该查询检查浏览器是否有hover功能。我们希望在支持hover的设备上才有翻转效果。对于不支持hover的设备(如手机),我们将同时显示正面和背面。

列表 9.22 将背面定位在正面之上

@media (hover: hover) {
  .back {
    position: absolute;
    top: 0;
    left: 0;
    transform: rotateY(-180deg);     ①
  }
}

① 翻转卡片

在本章的早期,我们在.card-item规则中设置了position属性的值为relative。在父元素或祖先元素上使用相对定位与我们将卡片背面position属性的值设置为absolute的事实相辅相成。topleft位置为0将是带有card-item类的顶部左区域(该容器包含两个卡片面)。

每当我们使用position: absolute时,我们将元素从页面的常规流中移除,并可以在页面上设置一个特定的位置来放置该元素。位置是基于具有position值为relative的最接近的祖先元素来计算的。如果没有找到,则页面的左上角将是顶部左角。

在这里有点令人困惑的是,如果没有设置值来定位元素(topleftrightbottominset),元素将放置在它通常所在的位置,但在流中不占用任何空间。元素的高度和宽度也会受到影响。如果 CSS 中提供了值,元素将保持该值;否则,它只占用所需的空间。即使它是一个块级元素,它也不再占用可用的全部宽度。此外,如果宽度使用相对单位(如百分比)设置,它将相对于它相关的元素进行计算。图 9.25 显示了使用position: absolute的一些场景。

图 9.25 绝对定位

当我们的 CSS 应用(图 9.26)卡片背面翻转并位于正面之上时,我们可以应用backface-visibility属性。

图 9.26 卡片背面位于正面之上并翻转

9.5.2 过渡和 backface-visibility

到目前为止,我们只看了 2D 空间中的对象——换句话说,一个平面的视角。我们看了宽度和高度,但没有深度。现在我们将考虑第三个维度。

当背面翻转时,我们需要它在用户悬停在卡片上时才显示。我们有两个侧面,第二个侧面有一个transform: rotateY(-180deg)声明(背面)。在 3D 空间中,因此,这个侧面是背对着我们的。如果我们将backface-visibility属性值设置为hidden在两个侧面,那么背对着我们的侧面将被隐藏。

我们目前背对着我们的背面是隐藏的。如果我们旋转整个卡片,背面朝向我们,正面被隐藏。图 9.27 说明了我们的 CSS 和 HTML 如何交互以创建翻转效果。

图 9.27 将backface-visibility属性应用于我们的用例

在我们的 CSS 中,我们向媒体查询(列表 9.23)添加了以下规则和属性。它们指示卡片在背对我们时隐藏侧面,并在鼠标悬停时绕 y 轴旋转整个卡片 180 度。注意一个我们还没有讨论过的属性:transform-style,我们给它赋值为preserve-3d。没有这个属性,翻转将不会工作。它告诉浏览器我们正在 3D 空间中操作,而不是 2D 空间,从而建立了前后概念。

列表 9.23 隐藏背面并在hover时显示

@media (hover: hover) {
  ...
  .card-item {
    transform-style: preserve-3d;     ①
  }
  .card-item__side {
    backface-visibility: hidden;      ②
  }
  .card-item:hover {                  ③
    transform: rotateY(180deg);       ③
  }                                   ③
}

① 指示浏览器像在 3D 空间中操作一样

② 隐藏背对着我们的侧面

③ 在悬停时,翻转整个卡片以显示背面

当我们的悬停功能暴露卡片背面(图 9.28)时,我们需要添加动画使其看起来更像卡片翻转。注意背面不再镜像。

图 9.28 卡片默认状态和hover状态

目前,当我们悬停在卡片上时,背面会立即显示。我们希望让它看起来像卡片实际上正在被翻动。

9.5.3 过渡属性

为了动画化卡片翻动,我们将使用一个过渡。你可能还记得第五章中提到的,过渡用于动画化 CSS 的变化。在这种情况下,我们将通过向card-item(包含两个面的容器)添加过渡声明来动画化卡片的旋转变化。我们还将向媒体查询添加一个条件。

由于这个动画动作较多,我们想要确保尊重用户的设置。因此,我们将在媒体查询中添加一个prefers-reduced-motion: no-preference条件,如下所示。

列表 9.24 过渡和transform

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  ...
  .card-item {
    transform-style: preserve-3d;
    transition: transform 350ms cubic-bezier(0.71, 0.03, 0.56, 0.85);
  }
  ...
}

我们动画的持续时间是 350 毫秒,它影响transform属性(旋转),并且仅对那些在设备上未将prefers-reduced-motion设置为reduce的用户可见。图 9.29 显示了动画的进度,图 9.30 显示了用户界面,当用户启用了prefers-reduced-motion时。

图 9.29 时间上的动画

对于我们的时间函数,我们使用了cubic-bezier()函数。接下来,让我们更详细地看看这个函数代表什么。

图 9.30 Chrome DevTools 中prefers-reduced-motion: reduce模拟的prefers-reduced-motion: reduce

9.5.4 cubic-bezier()函数

贝塞尔曲线是以法国工程师皮埃尔·贝塞尔的名字命名的,他在雷诺汽车的车身上使用了这些曲线(mng.bz/d1NX)。贝塞尔曲线由四个点组成:P[0]、P[1]、P[2]和 P[3]。P[0]和 P[3]代表起始点和结束点,而 P[1]和 P[2]是点的句柄。点和句柄的值通过 x 和 y 坐标设置(图 9.31)。

图 9.31 贝塞尔曲线上的点和句柄

在 CSS 中,我们只需要担心句柄,因为 P[0]和 P[3]的值分别被设置为(0, 0)(1, 1)。通过操纵曲线,我们改变动画的加速度。在 CSS 中,我们的函数接受四个参数,代表 P[1]和 P[2]的xy值:cubic-bezier(x1, y1, x2, y2),其中x值必须在01之间,包括01

我们在上一章中用于过渡和动画的预定义时间函数都有cubic-bezier()值,它们可以通过这些值来表示(图 9.32)。¹

图 9.32 预定义的曲线

编写我们自己的cubic-bezier()函数来动画化我们的设计可能会很繁琐。幸运的是,像cubic-bezier.com这样的在线工具允许我们查看曲线并确定值(图 9.33)。

图 9.33 来自 cubic-bezier.com 的一个示例cubic-bezier()函数

我们还可以在某些浏览器开发者工具中看到cubic-bezier(),例如 Mozilla Firefox 中的那些(图 9.34)。

图 9.34 Firefox DevTools 曲线细节

我们的动画完成后,让我们给我们的项目添加一些收尾工作。

9.6 边框半径

大多数信用卡都有圆角,所以我们将我们的也做成圆角。我们还将圆角应用于卡片背面的白色 CVV 框。

在用户界面中添加圆角可能是一种平衡行为。我们将给卡片添加圆角,使其看起来更自然、更逼真。尖锐的角可能显得过于激进,但过度使用圆角可能会使界面看起来过于柔和和俏皮,这可能在所有情况下都不适用。正确的曲线量是设计特定的。为了使我们的卡片看起来更逼真,我们将添加以下 CSS。

列表 9.25 添加border-radius

.card-item__side {       ①
  height: 100%;
  width: 100%;
  background: url("bg.jpeg") left top / cover blue;
  border-radius: 15px;
 }
.card-item__cvvBand {    ②
  background: white;
  height: 45px;
  margin-bottom: 30px;
  padding-right: 10px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  color: #1a3b5d;
  font-style: italic;
  border-radius: 4px;
}

① 卡片

② 白色 CVV 带

有圆角后,我们的卡片看起来像图 9.35。

图 9.35 卡片和 CVV 带的圆角

9.7 盒子和文本阴影

在第四章中,我们简要介绍了drop-shadow值,该值可以应用于filter属性以进行图像过滤。将阴影应用于元素的另一方法是使用box-shadow属性,该属性将阴影应用于元素框。

9.7.1 drop-shadow 函数与 box-shadow 属性的区别

我们可能会想知道drop-shadow过滤器属性和box-shadow属性之间的区别。它们都有相同的基本值集,但box-shadow属性有两个非必选值:spread-radiusinset

使用带有drop-shadow属性的过滤器在图像上的好处是,当我们使用过滤器时,阴影应用于 alpha 蒙版而不是边界框。因此,如果我们有一个 PNG 或 SVG 图像,并且该图像有透明区域,阴影将应用于该透明区域周围。如果我们向同一图像添加box-shadow而不是过滤器,阴影将仅应用于外部图像容器(图 9.36)。

图 9.36 比较box-shadow(左)和drop-shadow(右)

为了加强卡片上的 3D 效果并使卡片看起来像是在漂浮,我们将给我们的卡片添加一个阴影。因为我们只关心给卡片的边界区域添加阴影,所以我们可以使用box-shadow属性,这将给项目带来深度感,并进一步强调有东西在背面。阴影将很大、很柔和,并且相当透明。为了达到这种效果,我们将向.card-item__side规则添加box-shadow: 0 20px 60px 0 rgb(14 42 90 / 0.55);。我们的更新规则如下所示。

列表 9.26 在我们的卡片上使用box-shadow

.card-item__side {
  height: 100%;
  width: 100%;
  background: url("bg.jpeg") left top / cover blue;
  border-radius: 15px;
  box-shadow: 0 20px 60px 0 rgb(14 42 90 / 0.55);
}

图 9.37 显示了我们的更新后的卡片。

图 9.37 添加阴影使卡片看起来像是在漂浮

9.7.2 文本阴影

我们还可以给文本添加阴影。如果我们给文本应用 box-shadow,阴影将应用于包含文本的框,而不是单个字母。要给字母添加阴影,我们使用 text-shadow 属性,其语法与 box-shadow 属性相同。我们将在卡片的正面使用这个属性来提升文本与背景的分离。我们需要将这个属性添加到我们的 .front 规则中,如下面的列表所示。

列表 9.27 卡片正面所有文本元素的文本阴影

.front{
  padding: 25px 15px;
  text-shadow: 7px 6px 10px rgb(14 42 90 / 0.8);
}

图 9.38 展示了添加阴影前后的卡片。

图片 9.38

图 9.38 添加 text-shadow 前后对比

虽然效果微妙,但添加的阴影使数字显得更加突出。值得注意的是,这种效果最好谨慎且适度地使用,因为它可能会妨碍可读性而不是帮助它。

9.8 总结

最后一个需要处理的问题是关于那些不与翻页效果交互但同时在查看卡片两面(如没有悬停功能的设备,如手机和平板,以及设置了 prefers-reduced-motion 的用户)的用户。目前当两面都显示时,卡片面之间没有空间。因此,让我们在面底部添加一些边距以分隔它们,如下面的列表所示。

列表 9.28 分隔卡片面

.card-item__side {
  height: 100%;
  width: 100%;
  background: url("bg.jpeg") left top / cover blue;     ①
  border-radius: 15px;
  box-shadow: 0 20px 60px 0 rgb(14 42 90 / 0.55);
  margin-bottom: 2rem;
 }

① URL 被截断以提高可读性

在 Moto G4 设备上,我们的卡片看起来像图 9.39。

图片 9.39

图 9.39 移动设备上的我们的项目

使用这个最后的添加,我们的项目就完成了。通过结合使用媒体查询、阴影、定位和过渡,我们创建了一个看起来逼真的卡片(图 9.40)。

图片 9.40

图 9.40 完成项目

摘要

  • 我们可以通过 box-sizing 属性改变盒模型的行为。

  • background 属性的值 cover 允许我们在覆盖整个元素的同时尽可能多地显示背景图像。

  • 虽然字体有多种格式,但对于网页来说,我们只需要 WOFF 和 WOFF2 格式。

  • 字体可以是静态的或可变的。

  • 我们使用 @font-face 规则来定义字体在哪里以及如何导入,以及它们应该如何表现。

  • @font-face 规则需要放在样式表的顶部。

  • @supports 规则允许我们创建针对浏览器功能的特定样式。

  • transform-style: preserve-3d 结合使用的 backface-visibility 属性创建了一个翻页效果。

  • cubic-bezier() 函数定义了我们的元素如何随时间动画。

  • box-shadow 属性允许我们给元素添加阴影。

  • text-shadow 而不是 box-shadow 是我们用来给单个文字字母添加阴影的属性。


¹ 《架构 CSS:程序员高效样式表指南》,作者 Martine Dowden 和 Michael Dowden (2020, Apress)。

10 表单样式

本章涵盖

  • 样式输入字段

  • 样式单选按钮和复选框

  • 样式下拉菜单

  • 考虑可访问性

  • 比较 :focus:focus-visible

  • 使用 :where:is 伪类

  • 使用 accent-color 属性

表单在我们的应用程序中无处不在。无论是联系表单还是登录屏幕,无论它们是否是应用程序功能的核心,它们确实无处不在。然而,表单的设计可以轻易地使用户体验变得更好或更差。在本章中,我们将对表单进行样式化,并查看我们需要确保解决的某些可访问性考虑因素。我们将探讨一些与样式化某些单选按钮和复选框输入以及下拉菜单相关的挑战,并介绍一些样式化错误消息的选项。

在这个上下文中,表单是一个 HTML <form>元素中的代码部分,包含用户与之交互以向网站或应用程序提交数据的控件(表单字段)。由于联系表单在应用程序和网站中非常普遍,我们将使用联系表单作为我们项目的基准。

10.1 设置

我们的形式包含两个输入字段、一个下拉菜单、单选按钮、复选框和一个文本区域。我们还在表单顶部有一个标题,并在表单末尾有一个发送按钮。图 10.1 显示了我们的起点——未应用任何样式的原始 HTML,以及我们想要实现的目标。

图 10.1 起始点和成品

我们的起始 HTML 相当简单;它包含我们的表单,其中放置了我们的标签、字段、错误消息和按钮。起始和最终代码在 GitHub(mng.bz/rWYZ)、CodePen(codepen.io/michaelgearon/pen/poeoNbj)和以下列表中。

列表 10.1 起始 HTML

<body>
  <main>
    <section class="image"></section>                                      ①
    <section class="contact-form">
      <h1>Contact</h1>
      <form>
        <p>Your opinion is important to us...</p>
        <label for="name">
          <img src="./img/name.svg" alt="" width="24" height="24">         ②
          Your Name                                                        ②
        </label>                                                           ②
        <input type="text"                                                 ②
               id="name"                                                   ②
               name="name"                                                 ②
               maxlength="250"                                             ②
               required                                                    ②
               aria-describedby="nameError"                                ②
               placeholder="e.g. Alex Smith"                               ②
        >                                                                  ②
        <div class="error" id="nameError">                                 ②
          <span role="alert">Please let us know who you are</span>         ②
        </div>                                                             ②

        <label for="email">                                                ③
          <img src="./img/email.svg" alt="" width="24" height="24">        ③
          Your Email Address                                               ③
        </label>                                                           ③
        <input type="email"                                                ③
               id="email"                                                  ③
               name="email"                                                ③
               maxlength="250"                                             ③
               required                                                    ③
               aria-describedby="emailError"                               ③
               placeholder="e.g. asmith@email.com"                         ③
        >                                                                  ③
        <div class="error" id="emailError">                                ③
          <span role="alert">Please provide a...</span>                    ③
        </div>                                                             ③

        <label for="reasonForContact">                                     ④
          <img src="./img/reason.svg" alt="" width="24" height="24">       ④
          Reason For Contact                                               ④
        </label>                                                           ④
        <select id="reasonForContact"                                      ④
                required                                                   ④
                aria-describedby="reasonError"                             ④
        >                                                                  ④
          <option value="">-- Pick One --</option>                         ④
          <option value="sales"> Sales inquiry</option>                    ④
             ...                                                           ④
        </select>                                                          ④
        <div class="error" id="reasonError">
          <span role="alert">Please provide the...</span>
        </div>

        <fieldset>                                                         ⑤
          <legend>                                                         ⑤
            <img src="./img/subscriber.svg" alt=""                         ⑤
  width="24" height="24">                                                  ⑤
            Are you currently a subscriber?                                ⑤
          </legend>                                                        ⑤
          <label>                                                          ⑤
<input type="radio" value="1" name="subscriber"                            ⑤
       checked required>                                                   ⑤
Yes                                                                        ⑤
          </label>                                                         ⑤
          <label>                                                          ⑤
            <input type="radio" value="0" name="subscriber" required>      ⑤
            No                                                             ⑤
          </label>                                                         ⑤
        </fieldset>                                                        ⑤
        <label for="message">                                              ⑥
          <img src="./img/message.svg" alt="" width="24" height="24">      ⑥
          Message                                                          ⑥
        </label>                                                           ⑥
        <textarea id="message"                                             ⑥
                  name="message"                                           ⑥
                  rows="5"                                                 ⑥
                  required                                                 ⑥
                  maxlength="500"                                          ⑥
                  aria-describedby="messageError"                          ⑥
                  placeholder="How can we help?"                           ⑥
        ></textarea>                                                       ⑥
        <div class="error" id="messageError">                              ⑥
          <span role="alert">Please let us know how we can help</span>     ⑥
        </div>                                                             ⑥

        <label>                                                            ⑦
          <input type="checkbox" name="subscribe">                         ⑦
          Subscribe to our newsletter                                      ⑦
        </label>                                                           ⑦

        <div class="actions">
          <button type="submit" onclick="send(event)">Send</button>
        </div>
      </form>
    </section>

  </main>

  <script src="./script.js"></script>                                      ⑧
</body>

① 左侧图像

② 与相关标签和错误消息关联的名称输入

③ 与相关标签和错误消息关联的电子邮件输入

④ 联系原因下拉菜单和相关标签

⑤ 包含订阅单选按钮的 Fieldset

⑥ 消息文本区域

⑦ 订阅勾选标记

⑧ 处理错误的 JavaScript

你可能已经注意到包含了一个 JavaScript 文件。我们将使用这个文件在章节的后面部分(10.8 节)展示和隐藏错误。

为了让我们能够专注于样式化表单元素,页面布局的 CSS 在起始项目中提供。我们使用 grid 将图像和表单并排放置。我们还使用渐变在背景中创建点。我们的主题颜色已通过 CSS 自定义属性和一些基本的排版设置设置,包括使用无衬线字体并将我们项目的默认文本大小更改为 12pt。以下列表显示了我们的起始 CSS。

列表 10.2 起始 CSS

html {
  --color: #333333;                                          ①
  --label-color: #6d6d6d;                                    ①
  --placeholder-color: #ababab;                              ①
  --font-family: sans-serif;                                 ①
  --background: #fafafa;                                     ①
  --background-card: #ffffff;                                ①
  --primary: #e48b17;                                        ①
  --accent: #086788;                                         ①
  --accent-contrast: #ffffff;                                ①
  --error: #dd1c1aff;                                        ①
  --border: #ddd;                                            ①
  --hover: #bee0eb;                                          ①

  color: var(--color);
  font-family: var(--font-family);
  font-size: 12pt;
  margin: 0;
  padding: 0;
}

body {
  background-color: var(--background);                       ②
  background-image: radial-gradient(var(--accent) .75px,     ②
                    transparent .75px);                      ②
  background-size: 15px 15px;                                ②
  margin: 0;
  padding: 2rem;
}

main {
  display: grid;                                             ③
  grid-template-columns: 1fr 1fr;                            ③
  margin: 1rem auto;                                         ④
  max-width: 1200px;                                         ④
  box-shadow: -2px 2px 15px 0 var(--border);
}

.image {                                                     ⑤
  background-image: url("/img/illustration.jpeg");           ⑤
  background-size: cover;                                    ⑤
  background-position: bottom center;                        ⑤
  object-fit: contain;                                       ⑤
}                                                            ⑤

.contact-form {
  background-color: var(--background-card);
  padding: 2rem;
}

h1 { color: var(--accent); }

① 使用自定义属性设置我们的主题颜色

② 添加点状背景

③ 将两个部分并排放置的网格

④ 防止我们的设计太宽,并在页面上水平居中

⑤ 将图像添加到左侧

10.2 重置字段集样式

字段集是专门设计来分组控件和标签的。单选按钮组是字段集的一个完美用例,因为它们使我们能够有效地明确地将控件分组。它们还为我们提供了一个通过 <legend> 标签为控件组添加标签的现成方式。然而,从风格上讲,我们可以同意它们相当不美观。

让我们重置该组的样式以使其在视觉上消失。从程序的角度来看,我们想要保留该组,因为它对辅助技术用户很有帮助,但我们将使其更好地融合。为了使 <fieldset> 样式消失,我们需要重置三个属性:bordermarginpadding。以下列表显示了我们的规则。

列表 10.3 重置字段集样式

fieldset {
  border: 0;
  padding: 0;
  margin: 0;
}

在移除 <fieldset> 的浏览器默认样式(图 10.2)后,让我们将注意力转向我们的输入字段。

图 10.2 重置字段集

10.3 输入字段样式化

我们在表单中有四种类型的输入字段,具体如下:

  • 您的姓名text

  • 您的电子邮件地址email

  • 是/否radio

  • 订阅我们的通讯checkbox

HTML 有更多类型的字段,包括 datetimenumbercolor,每种类型都有自己的语义意义和样式考虑。我们选择了前面的四种类型,因为它们在当今网络上被广泛使用。

这些字段的未样式化外观决定了我们将如何对它们进行样式化。例如,我们将单选按钮和复选框与文本输入区分对待,但我们可以在多个类型之间重用代码。我们将根据未样式化控件的外观对它们进行分组,因此我们将文本和电子邮件一起处理,然后一起处理单选按钮和复选框。让我们从文本和电子邮件输入开始。

10.3.1 文本和电子邮件输入字段样式化

我们首先想弄清楚如何仅选择文本和电子邮件输入字段——更确切地说,所有不是单选按钮或复选框的输入字段。一个解决方案是为我们想要处理的每个输入添加一个类。然而,这种方法难以维护,并且会变得相当嘈杂,尤其是在表单密集型应用程序或复杂表单中。因此,我们将使用伪类 :not() 与类型选择器 selector[type="value"] 结合使用。

:not() 伪类允许我们选择不符合特定标准的元素。在我们的情况下,我们想要选择所有没有 radiocheckbox 类型的输入字段。因此,我们的选择器将是 input:not([type="radio"], [type="checkbox"])。现在我们可以开始样式化输入字段,这些字段目前看起来如图 10.3 所示。

图 10.3 输入类型 text 和类型 email

我们在图 10.3 中看到字体大小小于我们在 body 上设置的 12pt 大小。小字体在移动设备上难以阅读;对于许多用户来说也很困难,尤其是年轻人和老年人。如果我们希望我们的表单易于在广泛的人群和设备上使用,我们需要将其增加,因此我们将它设置为 1rem 以匹配我们的应用程序的其余部分。输入默认不继承字体样式,因此我们还将显式设置 colorfont-familyinherit

注意 inherit 是一个实用的属性值。它允许一个元素在默认情况下不发生继承时强制从父元素继承属性值。

接下来,我们将为输入添加一些填充和自定义边框,以及使它们的角落圆滑。在这种情况下,我们将出于风格目的进行这些更改。大多数应用程序都有一个通用样式(外观和感觉)。我们选择应用于字段样式应该与我们的应用程序的其余通用主题保持一致,以帮助表单与页面融合,看起来像是属于那里的。从营销角度来看,坚持我们的主题也有助于加强品牌认知。

要创建底部边框渐变效果,我们将使用从我们的主色调到辅助色调的线性渐变。因为渐变是一个我们不能分配给 border-bottom 属性的图像,我们需要使用 border-image,这允许我们使用图像来样式化我们的边框。我们仍然会在 border-bottom 属性中提供一个颜色作为后备。我们的代码如下所示。

列表 10.4 为非 radiocheckbox 类型的输入字段设置样式

input:not([type="radio"], [type="checkbox"]) {

  font-size: 1rem;
  font-family: inherit;
  color: inherit;
  border: none;                                                             ①
  border-bottom: solid 1px var(--primary);                                  ②
  border-image: linear-gradient(to right, var(--primary), var(--accent)) 1; ③
  padding: 0 0 .25rem;
  width: 100%;
}

① 从字段中移除所有边框

② 恢复边框,但仅底部,使用我们的主色调作为后备颜色

③ 为我们的边框添加渐变

像素和 rem

注意到我们的边框使用像素,而其余的声明使用 rem。在某些情况下,我们希望设计中的某些元素相对于文本大小。换句话说,如果文本大小增加或减少,我们希望这些元素相应地缩放。在这种情况下,我们的填充和边距使用 rem,因为如果文本大小增加,我们不希望设计开始看起来拥挤;另一方面,如果文本大小减少,我们希望相应地缩小那个空间。对于这些情况,我们希望使用一个相对单位,如 rem。

我们希望边框保持 1 像素,然而,无论文本大小如何。因此,我们使用一个固定单位。

我们为文本和电子邮件输入设置了一些基本样式,如图 10.4 所示。我们已经开始为我们的表单控件开发一个主题。

图 10.4 文本和电子邮件输入样式

10.3.2 使选择框和文本区域与输入样式匹配

为了确保我们的控件在视觉和感觉上保持一致,让我们将应用于输入字段的相同样式应用到<textarea><select>元素上。我们不会创建新的规则或复制粘贴代码。为了保持我们的样式一致和可维护,我们将添加selecttextarea作为选择器到我们现有的规则中,如下面的列表所示。

列表 10.5 将textareaselect添加到现有规则

input:not([type="radio"], [type="checkbox"]),
textarea,                                                                   ①
select {                                                                    ①
  font-size: 1rem;
  font-family: inherit;
  color: inherit;
  border: none;          
  border-bottom: solid 1px var(--primary);                                  ②
  border-image: linear-gradient(to right, var(--primary), var(--accent)) 1; ③
  padding: 0 0 .25rem;
  width: 100%;
}

① 将文本区域和选择框添加到我们的规则中

② 将边框重新添加,但只在底部,以我们的主要颜色作为后备颜色

③ 为我们的边框添加渐变

当规则应用后,我们注意到这两个字段仍然需要一点额外的样式。让我们首先关注<textarea>。图 10.5 显示了我们的更新后的<textarea>

图 10.5 更新<textarea>样式

在默认的网页中,用户可以通过点击并拖动右下角来调整<textarea>的宽度和高度。在我们的布局中,增加或减少高度不会引起任何布局问题。然而,改变宽度会隐藏我们的图片,并最终使我们的表单失去居中,正如我们可以在图 10.6 中观察到的。

图 10.6 <textarea>调整大小问题

<textarea>以一种不美观的方式延伸到容器之外。当我们垂直调整大小时,容器会相应地调整大小,但水平方向并不是这样。通过将<textarea>resize属性值从默认设置(both)更改为vertical,我们限制了用户调整元素大小的能力。用户将继续能够改变其高度,但不能改变宽度,如下面的列表所示。

列表 10.6 更新textarea的样式

textarea { resize: vertical }

从视觉上看,文本框看起来相同,并且在右下角仍然有调整大小的控制(图 10.7)。然而,当用户与调整大小的控制交互时,他们将被限制在垂直调整大小。

图 10.7 <textarea>仅垂直调整大小

我们仍然需要处理<select>,但这个过程将比编辑<textarea>复杂得多。所以让我们先完成输入字段的样式设计,然后再回到完成<select>控件的样式设计。

10.3.3 样式化单选输入和复选框

一些表单控件因其可应用样式数量极其有限而闻名,难以进行样式设计。单选按钮和复选框正好属于这一类。直到最近,没有任何属性会影响单选按钮的圆圈或复选框的方块。我们唯一的选择是用我们自己的样式替换原生的控件样式。

为什么一些表单字段如此难以进行样式设计?

一些表单字段,包括单选按钮和复选框,因其难以样式化而享有声誉。这种声誉源于我们可以更改其外观的 CSS 属性数量有限。我们之所以只有有限的属性,是因为它们的外观大部分是由操作系统驱动的,而不是浏览器。

现在我们有了更改原生控件颜色的能力。accent-color 属性允许我们用我们指定的颜色替换用户代理选择的颜色。将 accent-color: var(--accent); 应用到我们的复选框和单选按钮(列表 10.7)会产生图 10.8 中显示的结果。

图 10.8 应用于单选按钮和复选框的强调颜色

列表 10.7 更新 textarea 的样式

input[type="radio"],             ①
input[type="checkbox"] {         ①
  accent-color: var(--accent);
}

① 样式仅应用于类型为单选或复选的输入。

元素已采用我们设置的强调颜色,而不是之前使用的默认浅蓝色。然而,如果我们增加应用程序中的 font-size,控件的大小并不会增加(见图 10.9)。

图 10.9 增加单选按钮和复选框的字体大小

虽然我们可以更改元素的颜色(这是一种快速有效地调整控件样式以更好地适应我们样式的有效方法),但如果我们想允许控件随着字体大小缩放或进行任何进一步的定制,我们需要用我们自己的样式替换控件的样式。因为我们想保持控件的功能,只替换其视觉方面,所以我们的 HTML 保持不变。我们将隐藏浏览器提供的原生控件,并用我们自己的自定义样式替换其视觉部分。为了隐藏原生控件,我们将使用 appearance 属性并将其值设置为 none。这个属性允许我们控制控件的原生外观。通过将其属性设置为 none,我们表示我们不希望它显示操作系统提供的样式。我们还将设置 background-color 为我们自己的背景颜色(因为一些操作系统为控件提供了背景),然后重置我们的边距。

我们可以移除之前创建的 accent-color 声明;我们正在从头开始重新创建控件的视觉方面,因此该声明将没有任何效果。以下列表显示了完成的重置。

列表 10.8 重置 radiocheckbox 输入

input[type="radio"],
input[type="checkbox"] {
  accent-color: var(--accent);
  appearance: none;
  background-color: var(--background);
  margin: 0;
}

图 10.10 显示单选按钮已消失。我们可以开始为这些控件创建自己的样式。

图 10.10 重置单选按钮和复选框的样式

首先,我们想要创建一个框。对于单选按钮输入,我们将给这个框一个 border-radius 使其呈圆形。本质上,无论输入元素是复选框还是一组单选按钮,输入都需要一个框。我们将通过给输入设置 heightwidth1.75em 来创建一个框。我们使用 em 单位,因为它们是父字体大小的百分比。通过将我们的高度和宽度设置为 1.75em,我们使它们等于父字体大小的 1 3/4 倍。如果我们的标签——容器以及因此也是我们的输入的父元素——的 font-size16px,我们的框将是 28 像素宽和 28 像素高(16 x 1.75 = 28)。

接下来,我们将添加一个继承自标签字体颜色的边框。这一步可能听起来有点奇怪:我们如何使 border-colorfont-color 继承?我们将使用关键字值 currentcolor,它允许属性在它们通常不能继承的情况下继承字体颜色。我们将边框颜色设置为 currentcolor 以使边框颜色与字体颜色匹配。为了设置我们的边框宽度,我们将使用 em 以允许边框宽度与单选按钮的大小成比例。

由于输入默认是内联元素,为了应用我们的高度和宽度,我们还需要更改 display 属性。我们将将其设置为 inline-grid,因为我们处理输入的 checked 状态时,需要将内部圆盘或勾选标记居中。网格允许我们通过 place-content 属性轻松实现这一点。

inline-grid 相对于 grid,就像 inline-block 相对于 blockinline-block 具有与 block 相同的所有特性,但将其自身放置在页面流中。inline-grid 以相同的方式工作。我们可以访问 grid 的所有功能,但元素将自身放置在页面流中,而不是在上一内容下方。就我们的目的而言,这意味着输入将与文本标签一起放置,而无需为我们必须为包含单选按钮输入或复选框的标签创建特殊规则。

最后,我们需要处理 border-radius。这一步是复选框和单选按钮开始分叉的地方,因为复选框是方形的,而单选按钮是圆形的。由于我们的字段有圆角,我们将为复选框添加一个小的 border-radius (4px)。为了使单选按钮呈圆形,我们将添加一个 border-radius50%。我们的更新规则如下所示。

列表 10.9 样式化的 radiocheckbox 输入

input[type="radio"],
input[type="checkbox"] {
  appearance: none;
  background-color: var(--background);
  margin: 0;
  width: 1.75em;
  height: 1.75em;
  border: 1px solid currentcolor;      ①
  display: inline-grid;                ②
  place-content: center;               ②
}

input[type="radio"] { border-radius: 50% }

input[type="checkbox"] { border-radius: 4px }

① 将边框设置为与父元素文本颜色相同的颜色

② 当元素被选中时,设置以居中内部圆盘或勾选标记

我们未选中的输入已经进行了样式化。现在我们需要处理当这些输入被选中时使用的样式。在图 10.11 中,选中的(已选中)和未选中的元素看起来是相同的。

图片

图 10.11 未选中的 radiocheckbox 样式

10.3.4 使用 :where() 和 :is() 伪类

在这个节点上,我们将查看两个将帮助我们保持代码干净和简洁的伪类::is():where()。这两个伪类在功能上相似,因为它们接受一个选择器列表,并在列表中的任何选择器匹配时应用规则。这两个都非常有助于编写长的选择器列表。我们不必像这样写

input:focus, textarea:focus, select:focus, button:focus { ... }

我们可以使用:where:is并写出等效的如下:

:where(input, textarea, select, button):focus { ... }

:is()伪类将以相同的方式应用。:is():where()之间的区别在于它们的特定性级别。:where()不太特定,因此容易覆盖。另一方面,:is()采用列表中最特定选择器的特定性值。

注意:要了解如何计算特定性,请查看第一章。在第 10.3.9 节中,我们将更深入地探讨使用:where():is()计算特定性的方法。

警告:在使用:is()时要小心,因为如果我们列表中的选择器有id选择器(id选择器是最特定的),我们可以创建难以覆盖的规则。

我们将使用:where():is()与伪类如:checked:hover:focus以及::before伪元素一起完成复选框和单选输入的样式。

10.3.5 样式选定的单选和复选输入

要添加选定的单选按钮的内部圆盘和复选框的勾选标记,我们将应用与用于未选中输入类似的方法。我们创建了一些适用于两种输入类型的基本样式,然后在样式分离时为每个元素单独添加了完成细节。像以前一样,我们首先创建一个框。接下来,我们将该框放置在现有样式的中心,然后将其塑造成圆盘或勾选标记的形状。

要创建这个要放置在我们当前元素内部的第二个框,我们将使用::before伪元素。在这个时候,:where()伪类(在第 10.3.4 节中介绍)开始发挥作用;我们将使用它来选择我们的两种输入类型,然后添加::before伪元素。我们的选择器将看起来像这样::where(input[type= "radio"], input[type="checkbox"])::before {}.

我们的内容将是空的,因此我们将使用content属性值为""(空引号),并给它一个display值为block,这样我们就可以分配一个widthheight

当我们之前创建外部框时,我们给了它一个高度和宽度为1.75em。我们使用em单位,以便控制相对于文本大小进行缩放。我们在这里也将做同样的事情。我们希望内部圆盘和勾选标记比它们的容器小,所以我们将heightwidth设置为1em。假设应用于输入的font-size16px,我们的框将是16px乘以1等于16

我们不需要做任何事情来定位我们的内部框。记住,之前我们设置了输入显示为 inline-grid,然后在列表 10.8 中添加了 place-content 属性,其值为 center。网格布局自动将内部框放置在输入的中心。我们的内部圆盘和勾选标记的 CSS 如下所示。

列表 10.10 内部框居中

:where(input[type="radio"], input[type="checkbox"])::before {
  display: block;
  content: '';
  width: 1em;
  height: 1em;
}

当我们应用此代码时,我们没有看到任何变化,如图 10.12 所示。我们的内部框确实存在,但目前还不可见。

图 10.12 不可见的内部框

该框不可见,因为它没有任何内容或背景颜色。我们将添加一个背景颜色。

10.3.6 使用 :checked 伪类

我们不会一直将相同的背景颜色应用到我们的元素上。当元素被选中时,我们将使用我们的强调颜色,而当元素被悬停时,我们将使用悬停颜色。

:checked 伪类选择器可以用于类型为 radiocheckbox 的输入,或者用于下拉菜单 (<select>) 中的 <option> 元素,以在元素被选中时应用样式。在 <option> 上使用它的能力取决于浏览器。

当我们为 checkedhover 状态应用 background-color 时,如果选择器具有相同的具体性级别(如我们的示例所示),则这些规则的编写顺序很重要。如果我们首先编写 checked 状态规则,然后编写 hover 状态规则,则悬停颜色将在悬停时应用于选中的输入;由于它出现在 CSS 文件中较后,悬停状态规则将覆盖 checked 状态规则。因此,我们想要确保在 CSS 文件中将悬停状态规则放置在 checked 状态规则之前。图 10.13 阐述了这两种情况。

图 10.13 关于悬停选中复选框背景的规则顺序

让我们看看我们如何在 CSS 文件中应用我们的背景颜色。以下列表显示了我们的 hoverchecked 代码到目前为止的情况。

列表 10.11 内部元素背景颜色

:where(input[type="radio"], input[type="checkbox"]):hover::before {      ①
  background: var(--hover);                                              ①
}                                                                        ①
:where(input[type="radio"], input[type="checkbox"]):checked::before {    ②
  background: var(--accent);                                             ②
}                                                                        ②

① 在悬停时为内部框添加背景颜色

② 在输入被选中时为内部框添加背景颜色

图 10.14 显示,我们可以在元素内部塑造一个框。当元素被选中时,该框以强调颜色显示,当用户悬停在未选中的单选按钮或复选框输入上时,我们看到一个灰色框。

图 10.14 设置选中状态

接下来,我们需要塑造内部框,我们的代码将分叉以创建单选按钮和复选框的圆盘和勾选标记。

10.3.7  塑造选中单选按钮的内部圆盘

从单选按钮输入开始,我们通过添加 border-radius50% 将内部框变成圆形,如列表 10.12 所示。我们不区分 hoverchecked 状态,因为我们希望形状无论元素状态如何都为圆盘。

列表 10.12 单选按钮内部圆盘

input[type="radio"]::before {
  border-radius: 50%;
}

现在我们有了看起来传统的单选按钮,无论文本大小如何都能很好地缩放(图 10.15)。在样式化单选按钮后,我们将注意力转向复选框内部的勾选标记形状。

图片 10-15

图 10.15 样式化的 radio 输入

10.3.8 使用 CSS 形状创建勾选标记

使我们的单选输入形状变得简单:我们使用 border-radius 实现圆盘形状。创建勾选标记并不那么简单。为此,我们将使用 clip-path

注意:clip-path 允许我们通过创建一个定义了元素哪些部分应该显示和哪些部分应该隐藏的剪切区域来创建形状。我们在第七章中使用了 clip-path

我们将应用于 clip-path 以创建勾选标记的形状是一个多边形。多边形是通过设置一系列基于 XY 百分比坐标来创建的,这些坐标之间创建了一条线。(0,0) 坐标是形状的左上角。如果形状没有明确关闭,它将自动连接第一个和最后一个点。我们的 polygon() 函数将是 polygon(14% 44%, 0% 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%)。图 10.16 解释了形状的点对点构建。

![图片 10-16.png]

图 10.16 多边形勾选标记形状坐标

注意:简单形状的坐标很容易确定。但是,当形状变得更加复杂时,手动确定坐标可能会很繁琐。在这些情况下,我们可以求助于矢量图形绘制程序,如 Inkscape 和 Illustrator,或者许多 CSS 形状生成网站之一,包括 bennettfeely.com/clippy

在创建形状后,我们可以创建我们的 clip-path 并将其应用于复选框的内部部分,如下列所示。

列表 10.13 检查框中的勾选标记

input[type="checkbox"]::before {
  clip-path: polygon(14% 44%, 0% 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}

添加了 clip-path 之后,我们就有了功能齐全的复选框。接下来,让我们添加一些收尾工作。注意图 10.17 中选中的单选按钮和复选框的轮廓仍然是我们字体颜色,而不是强调色。

![图片 10-17.png]

图 10.17 复选框中的样式化勾选标记

要在单选按钮和复选框被选中时为它们添加轮廓颜色,我们将再次使用 :checked 伪类,仅在控件被选中时将边框颜色更改为我们的强调色。这个过程对应于列表 10.14 中的代码。我们使用 :is() 而不是 :where() 是出于特定性的考虑。

列表 10.14 选中输入项的强调色轮廓

:is(input[type="radio"], input[type="checkbox"]):checked {
  border-color: var(--accent);
}

10.3.9 使用 :is() 和 :where() 计算特定性

我们之前提到:where()具有0的特定性,这意味着它是我们可用的最不特定选择器。我们在选择器input[type="radio"], input[type="checkbox"] { ... }中设置了默认边框颜色,其特定性为11`,根据表 10.1 计算得出。在每一列中,我们计算每种类型选择器的数量,列 A、B 和 C 形成特定性值。¹

表 10.1 计算特定性

选择器 AID 选择器(×100) BClass 选择器、属性选择器和伪类(×10) CType 选择器、伪元素(×1) 特定性
:where(input[type="radio"], input[type="checkbox"]) 忽略特定性规则,始终等于 0 000
:where(input[type="radio"], input[type="checkbox"]):checked 忽略特定性规则,始终等于 0 000
input[type="radio"] 0 1 1 011
input[type="radio"]:checked 0 2 1 021
:is(input[type="radio"], input[type="checkbox"]):checked 0 2 1 021

因为:is()基于其内部最特定选择器的值来确定特定性值,在这种情况下,特定性将是11加上:checked状态的另一个10,因此特定性为21。因为21大于0,我们覆盖了样式,我们的边框变成了我们的强调颜色。

现在我们对单选按钮和复选框进行了样式化,无论是选中还是未选中,以及悬停状态下的两种状态。图 10.18 显示了我们的进展情况。

让我们把注意力转向下一个下拉菜单。

图 10.18 样式化的复选框和单选按钮输入

10.4 样式化下拉菜单

虽然我们为<select>元素应用了与基于文本的<input><textarea>相同的默认样式(列表 10.5),但我们看到图 10.19 中下拉菜单(<select>)仍然很粗糙。在扩展视图中,我们还看到我们的选项列表与我们的主题不匹配。

图 10.19 下拉菜单关闭和展开

让我们先从修复背景颜色开始。虽然因为我们的表单背景是白色,所以不明显,但输入字段默认情况下有白色背景。我们将向现有的声明中添加一个规则,该规则影响<input><textarea><select>元素,将背景颜色设置为卡片背景(列表 10.15)。这样,如果卡片背景发生变化,我们的表单控件将具有适当的背景颜色。

列表 10.15 应用到select的默认样式

input:not([type="radio"], [type="checkbox"]),
textarea,
select {
  font-size: 1rem;
  font-family: inherit;
  color: inherit;
  border: none;
  border-bottom: solid 1px var(--primary);
  border-image: linear-gradient(to right, var(--primary), var(--accent)) 1;
  padding: 0 0 .25rem;
  margin-bottom: 2rem;
  width: 100%;
  background-color: var(--background-card);     ①
}

① 添加背景颜色声明

在添加了背景颜色后,我们看到输入和选项都有白色背景(图 10.20)。

图 10.20 select元素样式化

虽然将下拉菜单选项更新为更好地匹配我们的主题会很不错,但这些菜单,就像单选输入和复选框一样,从操作系统本身那里获得了许多样式和功能。因此,我们只能通过 CSS 进行样式化,对于这个设计,这些更改就是我们能够做到的极限。我们可以使用 JavaScript 和 ARIA 来替换整个控件,但由于这本书是关于 CSS 的,我们将尽可能只用 CSS 进行样式化。

ARIA 是什么?

ARIA(代表可访问的丰富互联网应用程序)是一组可以添加到 HTML 元素中的角色和属性,可以补充有关元素使用、状态和功能的信息,否则这些信息对用户不可用。更多信息,请参阅www.w3.org/WAI/standards-guidelines/aria

注意:在创建自定义控件时,重要的是要关注浏览器自动提供的底层可访问性信息和功能,并确保我们正在重新创建这些功能以及控件的外观方面。当需要自定义控件时,库或框架可能会有所帮助,前提是库或框架是在考虑可访问性的情况下构建的。通常,了解的最佳地方是文档。

10.5 样式化标签和图例

为了样式化我们的标签和图例,我们将首先为标签和控制之间提供垂直边距以留出空间。我们还将使用 Flexbox 来对齐文本和图标、单选输入和复选框。最后,我们将减小它们的字体大小并更改它们的颜色。在这里最重要的是用户输入的值,而不是标签。通过减小它们的大小,我们降低了它们在视觉层次结构中的重要性。我们最终得到的代码如下所示。

列表 10.16 添加边距并更新字体大小

label, legend {
  display: flex;          ①
  align-items: center;    ①
  gap: .25rem;            ①
  margin: 0 0 .5rem 0;
  font-size: .875rem;
  color: var(--label-color);
}

① 对齐标签文本和图标

在我们的标签和图例样式化(图 10.21)之后,让我们将注意力转向占位符。

图 10.21 样式化的标签和图例

10.6 样式化占位文本

在我们的表单中,很难区分哪些字段是用户填写的,哪些是占位文本。正如我们为标签所做的那样,我们将降低占位文本的强调程度,使其更容易与用户响应区分开来。

标签和占位文本

我们的项目既有标签也有占位文本。尽管占位文本可以有助于指导用户,但它不能替代标签。实际上,网络内容可访问性指南(WCAG)的可访问性标准特别要求表单字段要有标签(mng.bz/mVzW)。

用户在字段中输入值后,占位文本会消失。这种安排是有问题的,因为用户在输入值后没有参考说明的方法。

此外,标签对于辅助技术(如屏幕阅读器)是必需的,这些技术依赖于这些信息来向用户指示字段中预期的内容。

为了样式化我们的占位文本,我们将使用 ::placeholder 伪元素。因为我们希望占位符以相同的方式样式化,无论元素类型如何,我们将编写一个规则,针对所有占位文本,无论元素类型如何。在这个新规则中,我们将减小占位文本的大小并使其颜色变浅,如下列所示。

列表 10.17 样式化占位文本

::placeholder {                      ①
  color: var(--placeholder-color);
  font-size: .75em;
}

① 针对任何占位文本,无论元素类型如何

图 10.22 显示了我们的更新字段。

图 10.22 样式化的占位文本

接下来,让我们样式化表单底部的按钮。

10.7 样式化发送按钮

我们在表单底部有一个发送按钮。让我们让它更加突出,并使其与我们的表单匹配。我们将创建一个针对此按钮的规则。

接下来,我们将移除边框,使角落圆滑,并编辑文本和背景颜色。在图 10.23 的“之前”部分,按钮文本的大小小于我们的默认字体大小,因此我们也将其 font-size 改为 1rem。最后,我们设置了按钮的内边距。

图 10.23 样式化的发送按钮

为了使我们的按钮更加突出,我们将将其与我们的其他字段稍微分开。按钮位于一个具有 actions 类的 <div> 中。我们将为此 <div> 设置 2rem 的上边距,这将使按钮从订阅复选框下移得更远。以下列表显示了我们的新规则,图 10.23 展示了我们的进度。

列表 10.18 重置按钮样式

button[type="submit"] {
  border: none;
  border-radius: 36px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 1rem;
  cursor: pointer;
  padding: .5rem 2rem;
}

.actions { margin-top: 2rem }

接下来,让我们样式化错误信息。

10.8 错误处理

在姓名、电子邮件和消息控件下方是错误信息。目前,它们未进行样式化,因此不容易识别为错误信息,或者将它们与描述的错误字段匹配。此外,我们希望在用户与控件交互之前不显示此错误信息。没有人希望在开始之前就看到一个错误信息大声呼喊。

我们将样式化错误信息,使其看起来像错误信息;然后我们将默认隐藏它们,仅在适当的时候显示。这项任务就是我们的 JavaScript 文件发挥作用的地方。

我们将通过设置 --error 自定义属性将文本颜色设置为红色,就像大多数网页上的错误信息一样。我们还将使文本加粗,并在错误前加上错误图标以清晰地呈现它;我们不想仅使用颜色来传达意义或意图。

注意颜色是区分内容类型的好方法。但我们应该始终与它一起使用其他元素——例如图标;文本;或大小、粗细或形状的改变——因为色盲的人可能无法区分颜色。此外,某些颜色在不同文化中不具有相同的意义。出于可访问性和清晰性的考虑,最佳实践是使用除了颜色之外的其他方式来传达信息。

因此,为了保持我们的错误图标一致,而不是在每个错误前添加它,我们将通过 CSS 以编程方式添加它,使用 ::before 伪元素。为了调整图标的大小和位置,我们将使用两个相对单位:字符单位(ch),我们在第七章中使用过,它基于字体宽度;以及 ex,它相对于字体的 X 高度,即字体基线与平均线之间的距离(图 10.24)。我们使用这些特定的单位是因为它们不仅相对于字体大小,还相对于所使用的字体的特征。使用 chex 单位有助于使图标与文本之间的尺寸和间距看起来像是所用字体的延伸。

图 10.24 字体术语的视觉表示

我们还将为错误 <div> 添加一些边距,为输入字段留出一些空间。我们的错误样式规则如下所示。

列表 10.19 错误样式

.error {
  color: var(--error);         ①
  margin: .25rem 0 2rem;
}
.error span::before {
  content: url('./img/error.svg');
  display: inline-block;
  width: 1.25ex;               ②
  height: 1.25ex;              ②
  vertical-align: baseline;    ③
  margin-right: .5ch;
}

① 使文本变为红色

② 使图标为 1.25ex 高和 1.25ex 宽

③ 将图标与文本的基线对齐

注意,当我们将图标添加到文本之前时,我们将其添加到 span 中,而不是错误 <div> 本身,因为我们将在错误内部显示和隐藏 span 和整个错误 <div>。让我们更仔细地看看 HTML,以了解原因。

列表 10.20 展示了名称字段的完整控制,包括其标签和错误信息。请注意,错误 <div> 有一个 idnameError,它被输入字段的 aria-describedby 属性引用。aria-describedby 属性告诉屏幕阅读器和辅助技术,它引用的元素包含与输入字段相关的额外信息。

如果我们通过使用 display:none 完全隐藏错误 <div>,则 aria-describedby 所指向的元素将不存在。因此,我们只隐藏内容(span),这样就不会破坏元素与其错误之间的编程连接。因为我们只隐藏 span,所以我们需要将图标应用到 span 上,这样我们就可以在隐藏错误信息时隐藏图标。

列表 10.20 名称字段 HTML

<label for="name">Your Name</label>
<input type="text" id="name" name="name" maxlength="250" required
    aria-describedby="nameError">      ①
<div class="error" id="nameError">     ②
  <span role="alert">
    Please let us know who you are
  </span>
</div>

① 指出哪个

提供了关于输入的额外信息(通过 id 引用)

② 由 aria-describedby 属性引用的 ID

图 10.25 展示了我们的样式化错误信息。

图 10.25 样式化错误信息

在我们格式化错误信息后,我们可以只在不适当的时候显示它们。在图 10.25 中,我们看到输入值是有效的,但错误信息仍然出现。为了仅在字段无效时显示错误信息,我们首先将错误信息默认隐藏。我们将display属性值设置为none应用于包含在错误<div>中的span元素;然后我们使用:invalid伪类有条件地显示它(仅当字段无效时)。

在这种情况下,字段的验证性是由我们设置在字段本身的属性决定的。让我们再次查看名称输入的 HTML:<input type="text" id="name" name="name" maxlength="250" required aria-describedby="nameError">。我们包含了requiredmaxlength属性;因此,如果字段中没有值或者值的长度超过 250 个字符,字段值将无效,并且:invalid伪类中的样式将被应用。

电子邮件元素(<input type="email" id="email" name="email" maxlength="250" required aria-describedby="emailError">)也有maxlengthrequired属性,所以在与名称字段相同的条件下它将是无效的。它还有一个email类型。在 HTML 中,一些字段类型有内置的验证,email就是其中之一。如果我们输入一个电子邮件地址值"myEmail",它将是无效的。

使用:invalid伪类可以帮助我们在字段有效时防止错误显示,但它不能防止在用户尚未与字段交互时错误显示。我们可以使用:user-invalid伪类代替:invalid,这将触发一次,并且仅在用户与字段交互后,但在此写作时,只有 Mozilla Firefox 浏览器支持这个属性。因此,由于当前缺乏跨浏览器支持,我们转向使用 JavaScript。在未来,当:user-invalid属性得到更好的支持时,我们就不再需要使用 JavaScript 根据用户交互来显示/隐藏我们的错误信息。项目中包含的脚本监听 blur 事件,这发生在元素失去焦点时。当我们点击或切换到字段之外时,就会发生 blur 事件。我们的脚本监听这些事件,并给离开的字段添加一个dirty类,让我们知道哪些字段已被交互,哪些尚未。带有dirty类的字段已被交互;没有dirty类的字段尚未交互。

因为我们有这个“脏”类,结合:invalid伪类,我们只会在无效且用户已触摸的控件下方显示错误信息,从而防止我们在用户有机会填写表单之前显示错误信息。我们使用选择器.dirty:invalid + .error span。我们选择包含在具有error类的元素中的span,该元素紧接在一个既无效又具有dirty类的元素之后。

最后,当字段既无效又“脏”时,我们将字段的边框颜色更改为我们的错误颜色。因为我们使用边框图像创建渐变效果,所以需要移除它。以下列表显示了显示和隐藏错误信息的完整规则。

列表 10.21 错误处理 CSS

.error span { display: none; }          ①

.dirty:invalid + .error span {          ②
  display: inline;                      ②
}                                       ②

:is(input, textarea).dirty:invalid {    ③
  border-color: var(--error);           ③
  border-image: none;
}                                       ③

① 默认隐藏错误信息

② 当 HTML 中紧接其前的字段脏且无效时显示错误信息

③ 当输入和文本区域无效且脏时,更改其边框颜色为红色

图 10.26 显示了字段的三种可能状态:无效和脏,有效,以及无效但尚未触摸。

图 10.26 错误处理和字段状态

表面上,我们的表单似乎已经完成,但我们仍然需要添加一些收尾工作。

10.9 向表单元素添加悬停和焦点样式

因为我们希望我们的表单易于访问,所以我们需要确保包括悬停样式,并更新控件和按钮的默认焦点样式以匹配我们的主题。我们已经处理了单选按钮和复选框的悬停样式,但还没有处理焦点样式。对于其他元素,我们还没有考虑悬停和焦点状态。

让我们从焦点开始,因为我们仍然需要将焦点应用到我们表单上的所有元素。对于通过键盘而不是用鼠标点击元素来导航网页的用户来说,焦点很重要。它为用户提供了一个视觉指示器,显示哪个元素当前具有焦点。因此,如果我们不喜欢默认的焦点样式,可以重新设计它们,但不能移除它们。

10.9.1 使用 :focus 与 :focus-visible

由于无论用户如何导航网页,始终显示焦点样式可能会根据设计而显得过于繁重,CSS 规范最近增加了一个新属性,根据用户的交互模式(键盘或鼠标)应用焦点样式::focus-visible伪类允许我们在用户使用键盘交互时添加样式,但不会在用户使用鼠标时应用它。相比之下,:focus始终应用,无论用户与元素的交互方式如何。

对于我们的文本和电子邮件输入字段、下拉菜单和文本区域,我们将移除默认轮廓并将边框颜色从渐变更改为纯色。因为(正如我们在本章前面提到的)我们不想仅依靠颜色来区分,所以我们将边框样式从实线更改为虚线,如列表 10.22 所示。我们还需要考虑当字段被污染且无效时(显示错误消息并带有红色边框)应该做什么。我们希望保持错误状态下字段之间的颜色区分,因此我们编写了第二个规则以保持红色边框颜色。

列表 10.22 当文本字段和下拉菜单获得焦点时的样式

:is(
  input:not([type="radio"], [type="checkbox"]),
  textarea,
  select
):focus-visible {
  outline: none;                                                ①
  border-bottom: dashed 1px var(--primary);
  border-image: none;                                           ②
}

:is(                                                            ③
  input:not([type="radio"], [type="checkbox"]).dirty:invalid,   ③
  textarea.dirty:invalid,                                       ③
  select.dirty:invalid                                          ③
):focus-visible {                                               ③
  border-color: var(--error);                                   ③
}                                                               ③

① 移除默认轮廓

② 移除渐变图像

③ 当字段已被交互且其值无效时保持边框颜色

图 10.27 展示了我们的更新字段在获得焦点时的样子。

图 10.27 当文本字段和下拉菜单获得焦点时的样子

接下来,我们需要处理单选按钮和复选框的焦点状态。对于这些元素,我们将保持轮廓但编辑其外观。就像我们处理其他字段一样,我们将使用虚线和主要颜色。我们还将偏移轮廓以在边框和轮廓之间创建分隔,如列表 10.23 所示。

列表 10.23 当文本框和复选框获得焦点时的样式

:where(input[type="radio"],  [type="checkbox"]):focus-visible {
  outline: dashed 1px var(--primary);
  outline-offset: 2px;                   ①
}

① 将轮廓向外移动 2 像素,使其不紧挨着边框

图 10.28 展示了我们的单选按钮和复选框在获得焦点时的样子。

图 10.28 单选按钮和复选框的焦点样式

当焦点问题处理完毕后,让我们将注意力转向悬停。

10.9.2 添加悬停样式

用户输入文本的字段,例如类型为 textemail 的输入或 <textarea>,在悬停时已经将光标类型从默认更改为文本。图 10.29 展示了每种光标类型的外观。请注意,根据操作系统、浏览器和用户设置,光标可能看起来略有不同。

图 10.29 Chrome 中的光标

尽管我们的文本和电子邮件输入以及文本区域在悬停时已经有一些区分,但我们的下拉菜单没有。让我们将其光标更改为指针以强调该字段是可点击的,如以下列表所示。

列表 10.24 选择悬停样式

select:hover { cursor: pointer }

在处理了焦点和悬停之后,我们最后需要担心的是确保我们的样式对那些启用了 forced-colors: active 的用户也是有效的。

10.10 处理 forced-colors 模式

forced-colors 模式是一种高对比度设置,允许用户将调色板限制为他们在其设备上设置的系列颜色。Windows 的高对比度模式是这种用例的一个例子。当此模式启用时,它会影响许多 CSS 属性,包括我们在本项目中使用的一些属性,最值得注意的是 background-color。我们使用 background-color 来确定 radiocheckbox 输入的内侧部分在选中与未选中元素时是否可见。我们还用它来重新设计 select 控件的箭头。

在 Chrome 中,我们可以使用 DevTools 来模拟在我们的机器上启用 forced-colors 模式,而无需编辑我们的计算机设置。在我们的 DevTools 控制台中,选择渲染选项卡。如果它尚未显示,我们可以点击省略号按钮以显示可能的选项卡并从下拉菜单中选择它。在选项卡上,我们寻找 forced-colors 模拟下拉菜单并将其设置为 forced-colors: active。此设置更新页面样式,使其看起来就像我们在机器上设置了 forced-colorsactive。图 10.30 显示了启用模拟的 Chrome DevTools 设置。(注意:除了 Chrome 之外的其他浏览器可能没有此功能,或者启用它的技术可能不同。)

图片

图 10.30 Chrome DevTools 中的 forced-colors:active 模拟设置

当应用模拟时,我们的页面样式会改变(图 10.31)。我们无法判断哪个单选按钮被选中或复选框是否被勾选。这个例子说明了使用不仅仅是颜色来区分意义的重要性,因为我们的错误信息不再为红色。

图片

图 10.31 模拟的 forced-colors: active

我们不会尝试在这个模式下恢复我们的颜色,因为我们想尊重用户的设置。但我们需要确保选中的输入与未选中的输入可区分。

要创建仅当用户将 forced-colors 设置为 active 时才应用的规则,我们将使用媒体查询 @media (forced-colors: active) {}。在媒体查询内创建的规则将仅在用户启用了 forced-colors 时生效。

我们复选框和单选按钮不再可见的原因是系统定义的背景颜色(在这种情况下,为白色)正在应用于它们。因此,我们将更改我们的背景以使用系统颜色而不是我们的强调颜色。CSS 颜色模块第 4 级规范(mng.bz/o1Vy)列出了我们可用的颜色。我们将使用 CanvasText,这意味着我们将应用的颜色将与用于文本的颜色相同。以下列表显示了我们的完整媒体查询。

列表 10.25 forced-colors: active 媒体查询

@media (forced-colors: active) {
  :where(input[type="radio"], input[type="checkbox"]):checked::before {
    background-color: CanvasText;
  }
}

图 10.32 显示了应用媒体查询后的 forced-colors 模式下的我们的页面,修复了为我们的用户创建问题的样式。

图片

图 10.32 forced-colors: active样式固定

当我们关闭模拟时,之前设置的样式保持不变;它们不受媒体查询内设置的样式的影响(图 10.33)。

图 10.33 完成产品

完成这个最后的任务后,我们已经完成了表单的样式设计。

摘要

  • 与操作系统紧密耦合的功能表单控件,如下拉菜单,比那些缺乏这种耦合的控件更难样式化。

  • 我们可以通过使用渐变来创建形状。

  • 通过使用em,我们可以根据文本大小调整元素的大小。

  • 当无法以其他方式继承font-color时,我们可以使用关键字值currentcolor

  • :where():is()伪类的工作方式类似,但具有不同的特定性级别。

  • :checked伪类允许我们在元素被选中时定位表单元素。

  • :invalid伪类可以在字段无效时有条件地格式化字段。

  • 字段值的有效性由在 HTML 中设置的字段属性决定。

  • :focus样式对于使我们的设计可访问是必要的。

  • 我们可以使用:focus-visible使焦点样式仅对键盘用户显示。

  • 在某些浏览器中,我们可以强制浏览器应用悬停和焦点样式。

  • 仅使用颜色来传达意义是不够的,正如本项目中的错误信息所展示的那样。

  • forced-colors模式改变了某些属性的行为以及我们可以应用于用户界面的颜色。

  • forced-colors设置为active时,可以使用媒体查询有条件地应用样式。

  • 在某些浏览器中,我们可以模拟forced-colors模式来检查我们的设计。


¹ 《架构 CSS:程序员的样式表指南》,作者:Martine Dowden 和 Michael Dowden(2020 年,Apress 出版社)。

11 动画社交媒体分享链接

本章涵盖

  • 使用 OOCSS、SMACSS 和 BEM 架构模式

  • 在使用组件时范围化 CSS

  • 与社交媒体图标一起工作

  • 创建 CSS 过渡效果

  • 使用 JavaScript 克服 CSS 限制

互联网被创建的核心原因之一是为了分享和分发信息。我们今天做这件事的一种方式是通过社交媒体。在本章中,我们将样式化和动画化一些可以用于通过电子邮件或社交媒体分享网页的链接。

与前几章一样,我们将在这个项目中使用 HTML 和 CSS,而不使用任何框架。我们选择这种方法是为了专注于 CSS 本身,而不必处理使用外部包的复杂性和复杂性。但许多实际应用确实使用了框架,其中一些包括组件的概念。

将一个功能部分转换为组件的常见原因是为了在应用程序的多个地方重用代码或元素。随着可重用性的出现,可能会出现命名冲突。一些系统自动限制组件 CSS 的范围,防止任何可能的冲突。但许多系统不限制范围,将其留给开发者组织代码以防止在为新组件样式化时更改另一个组件的样式。

无论框架如何处理(或未处理)CSS 范围,我们都有各种架构选项来帮助我们组织和标准化我们的样式。在我们深入本章的项目之前,让我们快速了解一下一些 CSS 架构选项。

11.1 与 CSS 架构一起工作

最受欢迎的 CSS 架构方法之一是 OOCSS、SMACSS 和 BEM。我们将在本章中使用 BEM,但我们将查看所有三个选项,以便我们了解它们之间的高层次差异。

11.1.1 OOCSS

由 Nicolle Sullivan 在丹佛的 Web Directions North 介绍,OOCSS(面向对象 CSS;github.com/stubbornella/oocss/wiki)旨在帮助开发者创建快速、可维护和基于标准的 CSS。Sullivan 将 OOCSS 的对象部分描述为“一个重复的视觉模式,可以抽象成一个独立的 HTML、CSS 和可能的 JavaScript 片段。该对象可以在整个网站上重复使用”——换句话说,这就是我们今天可能认为的组件或小工具。为了实现这种可重用性,OOCSS 遵循两个主要原则:

  • 分离结构和皮肤——将视觉特性(背景、边框等,有时被称为主题)保留在其自己的类中,这些类可以与对象混合匹配以创建各种元素。

  • 分离容器和内容——通过避免使用位置依赖的样式,我们可以确保对象无论在应用程序或网站上的哪个位置看起来都一样。

11.1.2 SMACSS

由 Jonathan Snook 开发,SMACSS(CSS 的可伸缩和模块化架构;smacss.com)将 CSS 规则组织为五个类别:

  • 基础—使用元素、后代或子选择器和伪类应用默认值

  • 布局—用于在页面上布局元素,例如标题、文章和页脚

  • 模块—布局的更多离散部分,例如轮播图、卡片和导航栏

  • 状态—增强或覆盖其他样式的东西,例如错误状态或菜单的状态(打开或关闭)

  • 主题—定义外观和感觉;如果它是页面或项目的唯一主题,则不需要将其分开成自己的类

11.1.3 BEM

由名为 Yandex 的公司开发,BEM(块、元素、修饰符;en.bem.info/methodology)是一种基于组件的架构,旨在将用户界面分解为独立的、可重用的块:

    • 描述块的目的。

    • 例如,一个元素的类名,如header

  • 元素

    • 描述元素的目的。

    • 类名是块名后跟两个下划线和元素,例如header__text

  • 修饰符

    • 描述外观、状态和行为。

    • 类模式为block-name_modifier-name(例如:header_mobile)或block-name__element-name_modifier-name(例如:header__menu_open)。

选择 CSS 的架构方法是团队依赖的任务。项目的需求、团队的大小和经验,以及正在使用的库和框架都是需要考虑的因素。没有一种适合所有情况的解决方案,因此决策需要由团队做出。由于 BEM 的基于组件的特性,我们将在此章节中使用它来定义和样式化我们的社交媒体分享链接。

11.2 设置

现在我们已经选择了我们的方法论,它决定了我们将为项目使用的命名约定,让我们看看我们将要构建的内容。我们将样式化一个分享按钮,当点击时,会打开一组链接,允许用户通过电子邮件或 Facebook、LinkedIn 或 Twitter 分享页面。然后我们将使用过渡来动画化打开和关闭分享选项以及单个链接的悬停/聚焦效果。图 11.1 显示了我们的目标。

图 11.1 目标

我们起始的 HTML(列表 11.1)包括我们的组件容器、一个分享按钮和一个允许用户选择如何分享页面的菜单。代码包括一个链接的 JavaScript 文件,这使得我们的组件可以通过键盘导航使用,并在点击分享按钮时触发显示/隐藏组件内的链接。正如我们将在 11.6 节中看到的,仅使用 CSS 对元素进行动画有一些限制,因此我们将依赖几行 JavaScript 来支持我们的 CSS。我们将在本章的后面部分(也在 11.6 节中)更详细地了解 JavaScript;首先,我们将专注于我们的 HTML 和 CSS。

列表 11.1 起始 HTML

  <main>
    <div class="share" id="share">                                      ①

      <button id="shareButton"                                          ②
        class="share__button"                                           ②
        type="button"                                                   ②
        aria-controls="mediaList"                                       ②
        aria-expanded="false"                                           ②
        aria-haspopup="listbox">                                        ②
          <img src="./icons/share.svg" alt="" width="24" height="24">   ②
          Share                                                         ②
      </button>                                                         ②

      <menu aria-labelledby="share"                                     ③
            role="menu"                                                 ③
            id="mediaList"                                              ③
            class="share__menu">                                        ③

        <li role="menuitem" class="share__menu-item">                   ④
          <a href="mailto:?subject=Tiny%20..."                          ⑤
            target="_blank"                                             ⑤
            rel="nofollow noopener"                                     ⑤
            tabindex="-1"                                               ⑤
            class="share__link"                                         ⑤
          >
            <img src="./icons/email.svg"                                ⑥
                 alt="Email" width="24" height="24">                    ⑥
          </a>
        </li>
        <li role="menuitem" class="share__menu-item">   
          <a href="https://www.facebook.com/sh..."                      ⑦
            target="_blank"                                             ⑦
            rel="nofollow noopener"                                     ⑦
            tabindex="-1"                                               ⑦
            class="share__link"                                         ⑦
          >                                                             ⑦
            <img src="./icons/facebook.svg"                             ⑦
                 alt="Facebook" width="24" height="24">                 ⑦
          </a>                                                          ⑦
        </li>
        ...
       </menu>
    </div>
  </main>

  <script src="./scripts.js"></script>                                  ⑧

① 组件容器

② 分享按钮以打开和关闭社交媒体链接列表

③ 媒体菜单

④ 菜单项

⑤ 首个链接是一个通过电子邮件分享而不是社交媒体的 mailto 链接。

⑥ 媒体图标

⑦ 通过社交媒体分享的链接

⑧ 用于键盘交互和补充 CSS 的脚本

我们还应用了一些基本的起始 CSS 到 main 元素上,以便将组件从屏幕边缘移开:main { margin: 48px; }.

您可以在 GitHub 上找到所有起始代码(HTML、CSS 和 JavaScript),网址为 mng.bz/KeR4 或 CodePen,网址为 codepen.io/michaelgearon/pen/YzZzpWj。我们的起点看起来像图 11.2。

![11-02.png]

图 11.2 起点

正如您所看到的,图标已经提供,但让我们讨论我们是从哪里以及如何获得它们的。

11.3 图标来源

每次我们使用他人品牌的图标时,我们需要回答以下问题:

  • 我们是否有权使用该图标?

  • 图标的用途是否有任何限制?

当我们使用社交媒体图标时,这些品牌在我们的作品中被代表,因此我们必须遵循他们关于何时、如何以及在何种情境下可以使用品牌的指南。当我们使用不代表品牌的图标(例如我们用于 mailto 链接和分享按钮的图标)时,除非我们亲自创建了该图标,否则我们将受到版权法的约束,就像我们会对我们项目中使用的任何其他媒体(图像、声音、视频等)一样。

备注:我们不是律师,我们无意在本章中提供法律建议。如有疑问,请联系法律专业人士。

11.3.1 媒体图标

找到品牌图标如何使用的一个有效方法是,通过在网络上搜索诸如 风格指南品牌指南 等术语来查找该品牌的指南。许多社交媒体平台都有关于如何代表品牌的特定说明,包括图标和标志的下载。表 11.1 列出了我们组件中包含的社交媒体平台及其品牌信息链接。对于这个项目,我们直接从相应的品牌指南中获取了社交媒体图标。

表 11.1 社交媒体品牌资源

品牌 图标 资产链接
Facebook Facebook 图标 mng.bz/9Dza
领英 领英图标 brand.linkedin.com/downloads
Twitter Twitter 图标 mng.bz/jPry

11.3.2 图标库

寻找图标可能有点繁琐,尤其是在大型项目中,因此使用图标字体和库是常见的做法,这些库也受使用条款的约束。每个库和图标字体都有自己的规则,关于图标可以在哪里以及如何使用。一些还要求署名。因此,我们必须在寻找图标时意识到我们需要遵循的任何规则。

对于这个项目,我们从 Material Symbols (fonts.google.com/icons) 获取了非品牌相关的图标。因为我们只需要两个——分享 和电子邮件 @——所以我们下载了单个 SVG 并将它们包含在我们的图标文件夹中,而不是将整个库导入到项目中。图标已包含在启动代码中,因此我们准备好开始样式化。

11.4 样式化块

由于我们使用 BEM 作为命名约定,我们的块名称将是 "share"。因此,包裹整个组件的 <div> 容器将具有 share 类。这个块名称将包含在所有未来使用 BEM 命名约定的类中(第 11.1.3 节),这使我们的 CSS 作用域限于该组件,并有助于防止我们的组件与应用程序中可能使用的任何其他部分的样式冲突。

如列表 11.2 所示,我们为块定义了 font-familybackgroundborder-radius。我们还给组件一个 display 值为 inline-flexinline-flexflex 的工作方式相同,但使元素成为内联级元素而不是块级元素。通过使我们的组件表现得像内联元素(与链接、span、按钮等相同),我们在应用程序中的放置方面提供了最大的灵活性。此外,按钮默认是内联元素,当关闭时,呈现的实际上是按钮,因此我们将我们的组件赋予与按钮相同的流程行为。

注意:要了解 Flexbox 的工作原理以及其相关属性,请查看第六章。

列表 11.2 样式化容器

.share {
  font-family: Verdana, Geneva, Tahoma, sans-serif;
  background: #ffe46a;        ①
  border-radius: 36px;
  display: inline-flex;
}

① 黄色

块样式化后(图 11.3),让我们解决块内的单个元素。

图 11.3 样式化容器块

11.5 样式化元素

我们的块有三个后代元素,我们希望对它们进行样式化:

  • 分享按钮

  • 包含链接列表的菜单

  • 菜单内的单个链接

让我们从分享按钮开始,并按列表顺序进行。

11.5.1 分享按钮

分配给按钮的类名将包括块名称,后跟两个下划线,然后是元素。在我们的例子中,我们将这个元素称为 button,因此我们的类名将是 share__button。通过在类名前缀加上 share__,我们确保我们将会样式的按钮仅是我们块内的那个。

我们想要覆盖浏览器提供的默认值,并在按钮内对齐图标和文本(列表 11.3)。我们移除了背景和边框,调整了字体大小和填充,并使角落呈曲线。

为了对齐图标和文本,我们给按钮一个 display 值为 flex,然后使用 align-items 来垂直对齐图标和文本。为了在图标和文本之间添加空白,我们使用 gap 属性。

列表 11.3 样式化分享按钮

.share__button {
  background: none;
  border: none;
  font-size: 1rem;
  padding: 0 2rem 0 1.5rem;
  border-radius: 36px;
  display: flex;
  align-items: center;
  gap: 1ch;
}

图 11.4 展示了我们的输出。

图 11.4 样式化分享按钮

接下来,让我们处理悬停和聚焦样式。我们使用 :hover:focus-visible 伪类来有条件地更改光标样式,并为按钮添加黑色轮廓。然后我们将轮廓偏移 -5px,这样轮廓就会放置在按钮内部 5 像素处,而不是外部边缘。

outline-offset 属性允许我们控制轮廓的位置。正数将轮廓移得更远或远离元素;负数将轮廓内嵌。以下列表显示了我们的悬停和聚焦 CSS。

列表 11.4 分享按钮悬停和聚焦 CSS

.share__button:hover,
.share__button:focus-visible {
  cursor: pointer;
  outline: solid 1px black;
  outline-offset: -5px;
}

图 11.5 展示了鼠标悬停在按钮上的情况。

图 11.5 分享按钮悬停

11.5.2 分享菜单

为了样式化菜单及其项,我们想要移除项目符号,并将元素放置在分享按钮旁边的一行中。为了移除项目符号,我们给列表项一个 list-style 值为 none。然后我们给菜单一个 display 属性值为 flex。最后,我们移除了浏览器自动应用到菜单项上的默认边距和填充。以下列表显示了我们的 CSS。

列表 11.5 分享菜单和菜单项

.share__menu-item { list-style: none; }

.share__menu {
  display: flex;
  margin: 0;
  padding: 0;
}

当我们查看输出(图 11.6)时,我们注意到我们需要在容器边缘和元素之间留出一些空间。我们将在样式化单个链接时处理这个任务。

图 11.6 样式化菜单

11.5.3 分享链接

为了确保链接在悬停时有一个圆形边框(而不是椭圆形),我们将它们的 heightwidth 都设置为 48 像素。接下来,我们弯曲它们的角落。这一步也解决了我们的间距问题,因为我们看到在列表 11.6 中,我们已将图标 heightwidth 设置为 24。因为我们使链接在高度和宽度上都是 48 像素,所以当链接居中时,我们将在每个图标和其链接的边缘之间有 12 像素的空白。

列表 11.6 列表项 HTML

<li role="menuitem" class="share__menu-item">
  <a href="https:/ /www.facebook.com/sha..."
    target="_blank"
    rel="nofollow noopener"
    tabindex="-1"
    class="share__link"
  >
    <img src="./icons/facebook.svg" alt="Facebook" width="24" height="24">
  </a>
</li>

我们还给了链接一个透明的边框。边框会占用空间,因此为了防止在悬停或聚焦时内容移动,我们默认添加一个透明的边框,然后在需要显示时上色。这种方法确保了边框所需的空间被分配,并在边框显示时防止元素后面的内容移动。

为了在圆圈中间居中图标,我们使用 flex,使内容居中并使项目对齐。我们的 CSS 如下所示。

列表 11.7 链接样式

.share__link:link,
.share__link:visited {
  height: 48px;
  width: 48px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: solid 1px transparent;
}

在我们的链接样式化(图 11.7)后,我们可以为悬停和聚焦状态样式化链接。

图 11.7 样式化分享链接

11.5.4 scale()

在悬停和聚焦时,我们将通过将颜色从透明变为黑色来显示边框。当我们设置链接的边框时,我们使用了border简写属性,它允许我们在一个声明中定义样式、边框宽度和边框颜色。因为我们只改变颜色,所以我们将使用border-color而不是border简写。通过使用border-color,我们可以编辑边框的颜色,而不用担心其他已经定义的属性。

接下来,我们将使用scale()函数来增加图标的大小,使其看起来像是被放大了。在第二章中,当我们扩展加载条时,我们使用了scaleY()来垂直增长和缩小条。在这个项目中,我们希望链接按比例增长,所以我们将使用scale()。当传递单个参数时,这个函数会按相同比例水平垂直地增长元素。

scale()函数是scaleX()scaleY()的组合简写。如果只传递一个值,scale()的量将同时应用于垂直和水平。如果传递两个参数,第一个参数定义水平缩放,第二个定义垂直缩放。

在悬停或聚焦时,我们希望链接的大小比未交互时大 25%,所以我们将函数的单个参数设置为1.25并应用于transform属性。我们的 CSS 看起来如下所示。

列表 11.8 悬停和聚焦时链接的样式

.share__link:hover,
.share__link:focus-visible {
  border-color: black;
  outline: none;
  transform: scale(1.25);
}

应用了样式后,我们的链接在悬停时增长(如图 11.8 所示),但由于现在链接比容器高,链接的上下间隙没有黄色背景。

图 11.8 链接悬停效果

为了创建我们的放大效果,我们希望整个链接保持黄色。我们可以给链接添加一个黄色背景,这样就能完成这个任务,但背景需要是黄色,因为块的背景颜色是黄色。如果我们改变了容器的背景颜色,我们希望链接的背景颜色也改变。为了确保颜色保持同步,我们可以使用自定义属性(CSS 变量)或使元素从其父级继承颜色。

11.5.5 继承属性值

默认情况下,background-color属性不会继承。我们希望明确指示链接继承背景颜色。为此,我们可以将链接的background-color属性值设置为inherit。然而,继承只到父级。在我们的例子中,控制背景颜色的元素是链接的曾祖父母,如图 11.9 所示。

图 11.9 媒体链接的祖先

我们需要让 linkmenumenu-item 规则继承 background-color,以便它能够传递到链接上。在我们给所有三个元素赋予 background-color 值为 inherit(图 11.10)之后,我们注意到,尽管我们修复了悬停链接中的间隙,但我们却失去了组件右侧的曲线。

图 11.10 继承的 background-color

我们失去了曲线,因为和 background-color 一样,border-radius 也不会被继承。为了解决这个问题,我们应用了与 background-color 相同的逻辑。列表 11.9 展示了我们的编辑后的 CSS。注意,链接的 border-radius 没有被编辑。我们希望保持链接的形状为圆形,所以我们在链接上保留了 border-radius: 50% 声明。

列表 11.9 继承属性值

.share__menu-item {
  list-style: none;
  background: inherit;
  border-radius: inherit;
}

.share__menu {
  display: flex;      ①
  margin: 0;
  padding: 0;
  background: inherit;
  border-radius: inherit;
}

.share__link:hover,
.share__link:focus-visible {
  border-color: black;
  outline: none;
  transform: scale(1.25);
  background: inherit;
}

① 使链接成为圆形

虽然以这种方式继承值可能有点繁琐,但它确保了颜色可以从一个地方控制。如果我们决定更改背景的颜色,这种方法有利于维护性。它还为我们扩展组件以支持多个主题奠定了基础。另一个选择是使用自定义属性来设置颜色。

border-radiusbackground-color 继承之后,我们的悬停和焦点样式就完成了(图 11.11),但当我们悬停在链接上时,变化是突然的。让我们动画化大小变化。

图 11.11 分享链接悬停效果

11.6 动画化组件

在第二章中,我们使用关键帧创建动画,这允许我们定义动画的步骤。对于我们的悬停状态,我们已经定义了起始和结束状态。我们正在从一种状态(未悬停或聚焦)过渡到另一种状态(悬停或聚焦),其样式已经在规则中定义。因此,我们不是使用动画,而是使用过渡。

11.6.1 创建过渡

过渡不需要关键帧,但仍允许我们动画化从一种状态到另一种状态的样式变化。transition 属性允许我们定义哪些属性变化应该被动画化,以及持续时间和时间函数。通过将 transition: transform ease-in-out 250ms; 添加到我们的 .share__link 规则中,我们告诉浏览器动画化链接的大小变化(列表 11.10)。

为了选择过渡需要的时间长度,我们选择相对较快的:250 毫秒。我们希望动画足够慢以可见,但足够快以迅速。如果我们使过渡太慢,我们的项目看起来会滞后,并使用户从他们试图完成的任务(分享内容)中分心。

列表 11.10 链接大小变化的过渡

.share__link:link,
.share__link:visited {
  text-decoration: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 48px;
  width: 48px;
  border-radius: 50%;
  border: solid 1px transparent;
  transition: transform ease-in-out 250ms;
}

注意:您可能会注意到在添加过渡后,悬停时轮廓被截断。原因是 JavaScript 驱动组件的打开和关闭,并切换溢出和可见性。我们将在 11.6.2 节中详细介绍 JavaScript 正在做什么。点击分享按钮会切换这种行为。

在我们的过渡中,我们特别告诉浏览器要动画化 transform 属性上发生的更改,但在我们的 .share__ link:link, .share__link:visited 规则中我们没有 transform 属性。然而,当我们运行代码时,我们注意到我们的尺寸变化是动画化的,并且代码是有效的。这种行为发生是因为,当未定义时,scale() 默认等于 scale(1)。因此,当我们将鼠标悬停或聚焦到链接上时,我们正在从 scale(1) 动画到 scale(1.25),然后当我们从链接移开时,我们再次将缩放动画回 scale(1)

接下来,我们将动画化当按钮被点击时隐藏和显示链接。

11.6.2 打开和关闭组件

记住,我们的目标是使组件默认隐藏我们的链接菜单,并且仅在点击分享按钮时显示(图 11.12)。

图 11.12 关闭和展开状态

我们需要做的第一件事是默认隐藏菜单项。为了完成这个任务,我们将给 menu 设置 width0 并隐藏 overflow,如列表 11.11 所示。

列表 11.11 隐藏菜单

.share__menu {
  display: flex;
  margin: 0;
  padding: 0;
  background: inherit;
  border-radius: inherit;
  width: 0;                   ①
  overflow: hidden;           ②
}

① 将菜单的宽度设置为 0

② 隐藏溢出,以便链接内部也被隐藏

在我们的菜单隐藏(图 11.13)后,我们需要在点击分享按钮时切换显示和隐藏菜单。

图 11.13 隐藏菜单

我们的 JavaScript 为我们处理了部分行为。在本章开头,我们提到我们需要为这个项目编写一些 JavaScript。当我们打开 JavaScript 文件时,我们注意到它包含大量的代码(列表 11.12)。

列表 11.12 JavaScript 文件

(() => {
  'use strict';

  let expanded = false;
  const container = document.getElementById('share');
  const shareButton = document.getElementById('shareButton');
  const menuItems = Array.from(container.querySelectorAll('li'));
  const menu = container.querySelector('menu');

  addButtonListeners();
  addListListeners();
  addTransitionListeners();

  function addButtonListeners() {                                          ①
    shareButton.addEventListener('click', toggleMenu);                     ①
    shareButton.addEventListener('keyup', handleToggleButtonKeypress);     ①
  }                                                                        ①
  function addListListeners() {                                            ②
    menuItems.forEach(li => {                                              ②
        const link = li.querySelector('a');                                ②
        link.addEventListener('keyup', handleMenuItemKeypress);            ②
        link.addEventListener('keydown', handleTab);                       ②
        link.addEventListener('click', toggleMenu);                        ②
      })                                                                   ②
  }                                                                        ②

  function addTransitionListeners() {                                      ③
    menu.addEventListener('transitionstart', handleAnimationStart);        ③
    menu.addEventListener('transitionend', handleAnimationEnd);            ③
  }                                                                        ③

  function handleToggleButtonKeypress(event) {                             ④
    switch(event.key) {                                                    ④
      case 'ArrowDown':                                                    ④
      case 'ArrowRight':                                                   ④
        if (!expanded) { toggleMenu(); }                                   ④
        moveToNext();                                                      ④
        break;                                                             ④
      case 'ArrowUp':                                                      ④
      case 'ArrowLeft':                                                    ④
        if (expanded) { toggleMenu(); }                                    ④
        break;                                                             ④
    }                                                                      ④
  }                                                                        ④

  function handleMenuItemKeypress(event) {                                 ⑤
    switch(event.key) {                                                    ⑤
      case 'ArrowDown':                                                    ⑤
      case 'ArrowRight':                                                   ⑤
        moveToNext();                                                      ⑤
        break;                                                             ⑤
      case 'ArrowUp':                                                      ⑤
      case 'ArrowLeft':                                                    ⑤
        if (event.altKey === true) {                                       ⑤
          navigate(event);                                                 ⑤
          toggleMenu();                                                    ⑤
        } else {                                                           ⑤
          moveToPrevious();                                                ⑤
        }                                                                  ⑤
        break;                                                             ⑤
      case 'Enter':                                                        ⑤
        toggleMenu();                                                      ⑤
        break;                                                             ⑤
      case ' ':                                                            ⑤
        navigate(event);                                                   ⑤
        toggleMenu();                                                      ⑤
        break;                                                             ⑤
      case 'Tab':                                                          ⑤
        event.preventDefault();                                            ⑤
        toggleMenu();                                                      ⑤
        break;                                                             ⑤
      case 'Escape':                                                       ⑤
        toggleMenu();                                                      ⑤
        break;                                                             ⑤
      case 'Home':                                                         ⑤
        moveToNext(0);                                                     ⑤
        break;                                                             ⑤
      case 'End':                                                          ⑤
        moveToNext(menuItems.length - 1);                                  ⑤
        break;                                                             ⑤
    }                                                                      ⑤
  }                                                                        ⑤

  function handleTab(event) {                                              ⑥
    if (event.key !== 'Tab') { return; }                                   ⑥
    event.preventDefault();                                                ⑥
  }                                                                        ⑥

  function toggleMenu(event) {                                             ⑦
    expanded = !expanded;                                                  ⑦
    shareButton.ariaExpanded = expanded;                                   ⑦
    container.classList.toggle('share_expanded');                          ⑦
    if (expanded) {                                                        ⑦
      menuItems.forEach(li => li.removeAttribute('tabindex'));             ⑦
    }                                                                      ⑦
    if (!expanded) {                                                       ⑦
      menuItems.forEach(li => {                                            ⑦
        li.removeAttribute('data-current');                                ⑦
        li.tabIndex = -1;                                                  ⑦
      })                                                                   ⑦
      shareButton.focus();                                                 ⑦
    }                                                                      ⑦
  }                                                                        ⑦

  function moveToNext(next = undefined) {                                  ⑧
    const selectedIndex = menuItems.findIndex(                             ⑧
      li => li.dataset.current  === 'true'                                 ⑧
    );                                                                     ⑧
    let newIndex                                                           ⑧
    if (next) {                                                            ⑧
      newIndex = next;                                                     ⑧
    } else if (                                                            ⑧
      selectedIndex === -1 || selectedIndex ===  menuItems.length - 1) {   ⑧
      newIndex = 0;                                                        ⑧
    } else {                                                               ⑧
      newIndex = selectedIndex + 1;                                        ⑧
    }                                                                      ⑧

    if (selectedIndex !== -1) {                                            ⑧
      menuItems[selectedIndex].removeAttribute('data-current');            ⑧
    }                                                                      ⑧
    menuItems[newIndex].setAttribute('data-current', 'true');              ⑧
    menuItems[newIndex].querySelector('a').focus();                        ⑧
  }                                                                        ⑧

  function moveToPrevious() {                                              ⑨
    const selectedIndex = menuItems.findIndex(li => li.dataset.current);   ⑨
    const newIndex = selectedIndex < 1                                     ⑨
      ? menuItems.length – 1                                               ⑨
      : selectedIndex - 1;                                                 ⑨
    if (selectedIndex !== -1) {                                            ⑨
      menuItems[selectedIndex].removeAttribute('data-current');            ⑨
    }                                                                      ⑨
    menuItems[newIndex].setAttribute('data-current', 'true');              ⑨
    menuItems[newIndex].querySelector('a').focus();                        ⑨
  }                                                                        ⑨

  function navigate(event) {                                               ⑩
    const url = event.target.href;                                         ⑩
    window.open(url);                                                      ⑩
  }                                                                        ⑩

  function handleAnimationStart() {                                        ⑪
    if (!expanded) { menu.style.overflow = 'hidden' };                     ⑪
  }                                                                        ⑪

  function handleAnimationEnd() {                                          ⑫
    if (expanded) { menu.style.overflow = 'visible' }                      ⑫
  }                                                                        ⑫
})()

① 为分享按钮添加点击和按键监听器,通过键盘和鼠标打开和关闭菜单

② 为链接添加事件监听器,以处理菜单内的点击和按键导航

③ 为菜单添加事件监听器,以知道何时开始和结束过渡

④ 处理键盘上下箭头功能或分享按钮

⑤ 处理链接上的按键,以在菜单内进行键盘导航,包括退出菜单

⑥ 阻止使用制表符在链接之间导航,因为在制表符上,我们希望将焦点返回到分享按钮而不是转到下一个链接

⑦ 打开和关闭菜单

⑧ 当定义了下一个链接时,通过索引将焦点移至特定项目;否则,遍历链接,当用户到达菜单中的最后一个项目时返回顶部

⑨ 当用户在菜单中到达第一个项目时,将焦点移至上一个链接并返回列表底部

⑩ 当动作由键盘触发而不是默认点击或按键时导航用户;当用户在菜单项上按下空格键时使用

⑪ 当菜单关闭时隐藏溢出

⑫ 如果打开,显示溢出以允许放大图标扩展到容器外

大部分代码处理组件的键盘可访问性,列表 11.13 展示了与按钮点击相关的部分。当页面加载时,我们将组件默认设置为关闭状态,并找到元素的容器,将其分配给 container 变量。然后我们给按钮添加事件监听器,以便当按钮被点击时,触发 toggleMenu() 函数。当按钮被点击时,我们改变 expanded 变量的值为其相反数。如果设置是 true,则变为 false,反之亦然。最后,我们添加或移除 share_expanded 类。classList.toggle() 如果类不存在则添加该类,如果存在则移除。

列表 11.13 打开和关闭菜单(JavaScript)

(() => {
  ...
  let expanded = false;                                      ①
  const container = document.getElementById('share');        ②
  ...
  function addButtonListeners() {                            ③
    shareButton.addEventListener('click', toggleMenu);       ③
    ...                                                      ③
  }                                                          ③

  function toggleMenu(event) {
    expanded = !expanded;                                    ④
    ...
    container.classList.toggle('share_expanded');            ⑤
    ...
    ...
  }

① 定义一个变量来保存我们的当前状态

② 定义一个变量用于我们的 HTML 容器元素

③ 定义按钮点击时发生的情况

④ 切换 expanded 变量的值

⑤ 处理添加和移除 share_expanded

注意 因为这本书是关于 CSS 的,所以 JavaScript 包含在启动代码中。如果你在跟随,你不需要对 JavaScript 进行任何编辑以使其工作。

总的来说,这段代码在点击分享按钮时将 share_expanded 类添加到容器中。如果 share_expanded 已经打开,代码将移除它。我们之前隐藏了菜单项,但现在当 share_expanded 类存在时,我们将显示它们。

注意 记住我们决定使用 BEM 作为我们的类名约定。我们的类名只有一个下划线,因为 expanded 是我们的修饰符。我们使用修饰符是因为我们根据状态(打开/关闭)改变(修改)样式。我们有块(share)和修饰符(expanded);因此,我们的类名是 block_modifiershare_expanded

要在组件标记为 expanded 时显示链接,我们必须增加菜单的宽度,如列表 11.14 所示。我们还添加了一些水平填充,以在菜单周围留出一些空间。

为了计算菜单的宽度,我们将链接的数量乘以它们的宽度。链接的宽度是 48 像素(我们硬设置为这个值)加上边框(每边 1 像素)。因此,菜单的宽度是 width = 4 ×(48 + 2) = 200px

列表 11.14 显示菜单

.share_expanded .share__menu {
  width: 200px;
  padding: 0 2rem 0 1rem;
}

点击按钮并在第一个链接上悬停后,我们看到我们的链接不再在菜单外扩展(图 11.14)。我们还看到,在我们悬停在链接上并关闭菜单后,我们的菜单项继续显示,直到我们再次悬停在其上。

图片

图 11.14 点击时扩展的组件

记住,我们的 JavaScript 在过渡开始和结束时触发,并负责控制我们的溢出。虽然我们已经在悬停单个菜单项时动画化了样式变化,但我们还没有添加打开和关闭菜单的过渡。当我们添加这个过渡时,当过渡激活并完成时,溢出将被正确设置,这些问题就会消失。

我们需要完成的下一个任务是保持组件打开时通常在悬停时出现的按钮轮廓。因为我们已经有了一个在悬停和聚焦时添加边框的规则,我们将编辑这个规则以在组件打开时触发。通过重用规则,我们确保在悬停、聚焦状态以及列表可见时样式的一致性。为了添加条件,我们在规则中添加了.share_ expanded .share__button选择器,如下所示。

列表 11.15 在列表显示时添加按钮边框到分享按钮

.share__button:hover,
.share__button:focus-visible,
.share_expanded .share__button {
  cursor: pointer;
  outline: solid 1px black;
  outline-offset: -5px;
}

添加选择器后,组件展开后我们的按钮仍然保留边框(如图 11.15 所示);当组件关闭且未聚焦或悬停时,边框保持不存在。

图片

图 11.15 在列表显示时保持分享按钮边框

11.6.3 动画菜单

现在我们已经为打开和关闭状态设置了样式,让我们来动画化菜单的显示和隐藏。我们希望链接列表从左侧展开,如图 11.16 所示。

图片

图 11.16 打开动画分解

当菜单关闭时,我们将执行打开动画的反向操作,撤回菜单并隐藏链接。我们还将使用过渡执行链接的放大效果。我们不需要使用关键帧,因为动画只会在按钮点击时执行一次,并且我们已经定义了两种状态。

我们将添加以下transition声明到菜单中:transition: width 250ms ease-in-out。再次强调,我们希望过渡迅速,所以我们给它 250 毫秒的持续时间。

添加过渡后,我们发现图标在应该出现之前就变得可见了。图 11.17 分解了这种效果。

图片

图 11.17 图标显示过早

即使我们将过渡改为转换所有属性而不是仅width,同样的问题仍然会发生。原因是溢出。当菜单关闭时,我们希望菜单的溢出被隐藏;当它打开时,我们希望它可见。但是溢出不能像宽度一样逐渐改变。它要么可见,要么不可见,没有中间状态。

当打开菜单时,我们希望在将 overflow 更改为 visible 之前等待过渡完成。当我们关闭时,我们希望溢出立即隐藏。这项任务是我们转向 JavaScript 以支持我们的 CSS 的地方。我们将从 .share_expanded .share__menu 类中移除 overflow: visible,并通过 JavaScript 处理添加。

列表 11.16 突出了处理溢出的相关 JavaScript。魔法在于 transitionstarttransitionend 事件监听器。它们附加到菜单上,监听过渡何时被触发以及何时完成变更。当事件发生时,它们触发其函数来处理菜单的溢出。

列表 11.16 处理溢出的 JavaScript

(() => {
  'use strict';

  let expanded = false;
  const container = document.getElementById('share');
  const menu = container.querySelector('menu');
...
  addTransitionListeners();
...
  function addTransitionListeners() {
    menu.addEventListener('transitionstart', handleAnimationStart);
    menu.addEventListener('transitionend', handleAnimationEnd);
  }
...
  function handleAnimationStart() {                       ①
    if (!expanded) { menu.style.overflow = 'hidden'; }    ②
  }
...
  function handleAnimationEnd() {                         ③
    if (expanded) { menu.style.overflow = 'visible'; }    ④
  }
})()

① 当过渡开始时触发

② 如果正在关闭过程中,隐藏菜单的溢出

③ 当过渡结束时触发

④ 如果刚刚打开,显示溢出

注意:如我们本章前面提到的,JavaScript 包含在启动代码中。如果您正在跟随,您不需要编辑 JavaScript;它应该可以工作。

下一个列表显示了使动画工作的 CSS。

列表 11.17 更新后的用于打开和关闭动画的 CSS

.share__menu {
  display: flex;
  margin: 0;
  padding: 0;
  background: inherit;
  border-radius: inherit;
  width: 0;
  overflow: hidden;
  transition: width 250ms ease-in-out;     ①
}

① 添加动画

经过这些最后的编辑以使动画平滑,我们已经完成了我们的动画社交媒体分享组件。最终产品如图 11.18 所示。

图 11.18 最终产品

摘要

  • 我们有几种方法来组织 CSS。三种常见的模式是 OOCSS、SMACSS 和 BEM。

  • 图标受版权保护,因此在使用社交媒体图标时请遵循品牌指南。

  • 我们可以通过使用 inline-flex 使通过 Flexbox 显示的元素表现得像内联级元素。inline-flex 使用与 flex 相同的属性。

  • 外观的定位可以通过 outline-offset 控制。

  • scale() 函数允许我们按比例放大或缩小元素。

  • inherit 属性值允许我们从父元素继承通常不会继承的值。

  • 过渡不需要关键帧,但仍允许我们从一个状态到另一个状态动画化 CSS 变更。

  • overflow 属性允许我们控制超出其容器范围的元素是显示还是隐藏。

  • 当使用 JavaScript 来扩展我们的过渡功能时,我们可以使用 ontransitionstartontransitionend 事件监听器来触发 JavaScript 变更以响应过渡的生命周期。

12 使用预处理器

本章涵盖

  • CSS 预处理器

  • Sass 扩展 CSS 功能的示例

到目前为止,在这本书中,我们一直使用纯 CSS 编写所有样式。然而,我们也可以使用预处理器。每个处理器都有自己的语法,并且大多数预处理器扩展了现有的 CSS 功能。最常用的有

它们被创建来简化代码的编写,使其更易于阅读和维护,以及添加 CSS 中不可用的功能。为与预处理器一起使用而编写的样式有自己的语法,并且必须构建或编译成 CSS。尽管一些预处理器提供浏览器端编译,但最常见的方法是将样式预处理器,并将输出 CSS 发送到浏览器(mng.bz/Wzex)。

使用预处理器的优点是它提供的附加功能,我们将在本章中介绍这些功能的示例。缺点是现在我们需要为我们的代码添加一个构建步骤。预处理器的选择基于项目所需的功能、团队的知识,以及(如果项目使用框架)哪些框架受到支持。对于我们的项目,我们将根据流行度来选择。当开发者被调查关于他们对 CSS 预处理器情感时,大多数倾向于 Sass(图 12.1),因此我们将使用它。

图片

图 12.1 预处理器情感(数据来源 mng.bz/8ry2

12.1 运行预处理器

我们的项目包括为如何文章添加样式——我们可能在维基或文档中看到(图 12.2)。

图片

图 12.2 完成的项目

如前几章所述,起始代码可在 GitHub(mng.bz/EQnl)和 CodePen(codepen.io/michaelgearon/pen/WNpNoGN)上找到。但运行项目会有所不同。因为我们将使用 Sass 编写样式,它输出 CSS 而不是直接编写,我们需要一个构建步骤。要运行此项目并跟随本章的代码,你有两种选择:

  • npm

  • CodePen

注意 npm(Node.js 包管理器)是一个软件库、管理器和安装程序。如果你不熟悉 npm,那没关系。你可以按照第 12.1.3 节中的说明在 CodePen 中运行此项目。

12.1.1 npm 的设置说明

chapter-12目录的命令行中,使用npm install;安装依赖项,然后使用npm start启动处理器。此命令启动一个监视器,将监视styles.scss(在beforeafter目录中)的变化,并输出styles.cssstyles.map.css文件。

第二个文件——styles.map.css——是一个源映射文件。由于 CSS 是从另一种语言生成的,源映射允许浏览器开发者工具告诉我们代码片段在预处理器文件(对于本项目,styles.scss)中的来源位置。

12.1.2 .sass 与.scss 的区别

虽然我们使用 Sass,但我们的文件扩展名是.scss。Sass 有两种语法我们可以选择——缩进和 SCSS——文件扩展名反映了语法。

缩进语法

有时被称为Sass 语法缩进语法,使用.sass文件扩展名。当使用这种语法编写规则集时,我们省略大括号和分号,使用制表符来描述文档的格式。以下列表显示了使用缩进语法的两个规则,第一个处理正文上的边距和填充,第二个更改段落的行高。

列表 12.1 使用缩进语法的 Sass

body
     margin: 0
     padding: 20px

p
      line-height: 1.5

SCSS 语法

第二种语法是 SCSS,它使用文件扩展名.scss。我们将在这个项目中使用这种语法。SCSS 语法是 CSS 的超集,它允许我们使用任何有效的 CSS,以及 Sass 的功能。以下列表显示了列表 12.1 中的规则在 SCSS 语法中的表示。

列表 12.2 使用 SCSS 语法的 Sass

body {
  margin: 0;
  padding: 20px;
}

p {
  line-height: 1.5;
}

代码看起来像 CSS,这正是重点。在 SCSS 中,我们可以像习惯那样编写 CSS,并且可以访问 Sass 提供的所有功能。由于其与 CSS 的相似性,并且它不需要开发者学习新的语法,SCSS 是两种语法选项中更受欢迎的一个。

12.1.3 CodePen 设置说明

要为 CodePen 设置项目,请按照以下步骤操作:

  1. 前往codepen.io

  2. 在一个新的笔中,使用chapter-12/before文件夹中的代码,将body元素内的 HTML 复制到 HTML 面板中。

  3. .scss文件中的基本样式复制到 CSS 面板中。

  4. 要使面板使用 Sass 的 SCSS 语法而不是 CSS,请点击 CSS 面板右上角的齿轮(图 12.3)。

    图片

    图 12.3 设置按钮

  5. 从 CSS 预处理器下拉菜单中选择 SCSS(图 12.4)。

    图片

    图 12.4 CodePen CSS 预处理器设置

  6. 点击 Pen 设置对话框底部的绿色“保存并关闭”按钮。

12.1.4 开始 HTML 和 SCSS

我们的项目由标题、段落、链接和图片组成(列表 12.3)。注意,在我们的head中,我们引用的是 CSS 样式表,而不是 SCSS。浏览器使用编译版本。

列表 12.3 开始 HTML

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Chapter 12: Pre-processors | Tiny CSS Projects</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="styles.css">                 ①
</head>

<body>
  <h1>Keeping it Sassy</h1>
  <h2>Step 1</h2>
  <img src="https:/ /bit.ly/3VUzJ7g" alt="blue print">
  <p>
    Lorem ipsum dolor sit amet...
    <a href="">tincidunt purus</a>
    eu, gravida enim. Vestibulum...
  </p>
  <p class="success">You did it!</p>                        ②
  <p>Ut maximus id erat et mollis...</p>
  <h2>Step 2</h2>
  <img src="https:/ /bit.ly/3F4vd0f" alt="crane">
  <p>Aenean non lorem tincidunt...</p>
  <p class="warning">Don't press the big red button</p>     ③
  <p>
    Proin pharetra, urna et sagittis lacinia...
    <a href="">orci luctus</a>
    et ultrices posuere cubilia curae...
  </p>
  <h2>Step 3</h2>
  <img src="https:/ /bit.ly/3N42oD1" alt="wrong way">
  <p>Nullam ut auctor nisi...</p>
  <p class="error">Mistakes have been made</p>              ④
  <p>Vestibulum interdum eleifend...</p>
</body>

</html>

① 链接到处理后的 CSS 文件

② 绿色成功提示

③ 橙色警告提示

④ 红色错误提示

我们的基本样式设置了我们的排版,并在页面变宽时限制了内容的宽度,如下列所示。

列表 12.4 开始 SCSS

@import url('https:/ /fonts.googleapis.com/css2?
➥ family=Nunito:wght@300;400;500;800&display=swap');

body {
  font-family: 'Nunito', sans-serif;
  font-weight: 300;
  max-width: 72ch;
  margin: 2rem auto;
}

p { line-height: 1.5 }

到目前为止,我们还没有使用 Sass 提供的任何扩展功能。事实上,如果我们查看 CSS 输出(列表 12.5),我们会注意到文件内容除了文件底部的映射参考之外都是相同的。这个注释告诉浏览器在哪里找到源映射。图 12.5 显示了我们的起始点。

图片

图 12.5 起始点

注意:如果您正在使用 CodePen,您可以通过点击 CSS 面板右上角的齿轮旁边的向下箭头(参见图 12.3)并从下拉菜单中选择“查看编译后的 CSS”来查看编译后的 CSS。

列表 12.5 开始的 CSS 输出

@import url("https:/ /fonts.googleapis.com/css2?
➥ family=Nunito:wght@300;400;500;800&display=swap");
body {
  font-family: "Nunito", sans-serif;
  font-weight: 300;
  max-width: 72ch;
  margin: 2rem auto;
}

p {
  line-height: 1.5;
}

/*# sourceMappingURL=styles.css.map */     ①

① 源映射参考

注意:如果您没有看到 CSS 文件被创建并且样式被应用,请确保您正在运行 Sass 监视器(npm start)。当监视器启动时,让它后台运行;当您在 SCSS 文件中保存更改时,它会自动更新 CSS 文件。您仍然需要手动刷新浏览器。

12.2 Sass 变量

预处理器早期之所以受欢迎,一个原因就是它们在浏览器支持自定义属性之前就有变量。Sass 变量与 CSS 自定义属性非常不同,因为它们有不同的语法并且功能不同。让我们首先看看语法。要创建一个变量,我们从美元符号($)开始,后面跟着变量名,一个冒号(:),然后是一个值(图 12.6)。

图片

图 12.6 Sass 变量语法

在功能方面,Sass 变量不了解文档对象模型(DOM),也不理解层叠或继承。它们是块作用域的:只有它们定义的括号内的属性知道它们的存在。因此,以下列表中提出的场景会在编译时抛出一个未定义变量错误,因为变量在两个不同的规则或块中定义和使用了。

列表 12.6 $myColor 变量在第二个规则中未定义

body {
  $myColor: blue;                ①
}

body p {
  /* $myColor is undefined */
  color: $myColor                ②
}

① 在 body 规则内部定义 $myColor 变量

$myColor 未定义,因为它是在不同的规则中创建的。

为了防止这个问题,我们可以将变量放在规则之外,这样它们就可以在整个文档中可用,如以下列表所示。

列表 12.7 定义变量

$myColor: blue;       ①

body p {
  color: $myColor;    ②
}

① 在任何规则集外部定义 $myColor 变量

$myColor 现已定义,其值为蓝色。

与动态的自定义属性不同,Sass 变量是静态的。如果我们定义了一个变量,使用它,更改其值,然后再使用它,那么在更改之前分配给它的任何属性都将保留原始值,而更改之后分配的将具有新值。列表 12.8 和 12.9 中显示的示例使这种情况更加清晰。请注意,这些示例不是我们项目的一部分;我们在这里展示它们只是为了说明概念。您可以在 CodePen 上找到代码:codepen.io/martine-dowden/pen/QWxLjWy

列表 12.8 自定义属性与变量(HTML)

<p class="first">My first paragraph</p>
<p class="second">My second paragraph</p>

列表 12.9 自定义属性与变量(SCSS)

body { --myBorder: solid 1px gray;  }        ①
$primary: red;                               ②

.first {                                     ③
  color: $primary;                           ③
  border: var(--myBorder);                   ③
}                                            ③

body { --myBorder: dashed 1px purple;  }     ④
$primary: blue;                              ⑤

.second {                                    ⑥
  color: $primary;                           ⑥
  border: var(--myBorder);                   ⑥
}                                            ⑥

① 将 --myBorder 自定义属性分配为实线灰色边框

② 将红色分配给我们的 $primary 变量

③ 将 --myBorder 自定义属性和 $primary 变量应用于颜色和边框属性

④ 将 --myBorder 自定义属性值更改为虚线紫色边框

⑤ 将 $primary 值更改为蓝色

⑥ 将 --myBorder$primary 应用于第二段

自定义属性和变量之间的第一个重大区别是我们不需要在规则内部定义我们的变量。此外,两个段落的边框样式是相同的,但文本的颜色却不是(图 12.7),尽管自定义属性和变量在第一和第二规则之间都被重新分配了。

图 12.7 示例输出

当我们重新分配自定义属性(边框)的值时,它将应用于所有地方,而颜色不会回溯性改变;只有更改后的规则受到影响。原因是自定义属性是动态的,而变量是静态的。

通过这种理解,让我们回到我们的项目,并为我们将使用的颜色定义一些变量。在文件顶部,我们将定义四个颜色变量。然后我们将应用主颜色到所有标题,如下列所示。

列表 12.10 颜色变量(SCSS)

@import url('https:/ /fonts.googleapis.com/css2
➥ ?family=Nunito:wght@300;400;500;800&display=swap');

$primary: #063373;              ①
$success: #747d10;              ②
$warning: #fc9d03;              ③
$error: #940a0a;                ④

p { line-height: 1.5 }

h1, h2 { color: $primary; }     ⑤

① 蓝色

② 绿色

③ 橙色

④ 红色

⑤ 使我们的标题变为蓝色

我们将变量放置在文件的开头,并放在任何规则之外,这样从那时起,我们就可以在任何规则内部访问它们。我们在 CSS 输出(列表 12.11)中注意到我们的变量在编译后的 CSS 中是不可见的。但在定义我们的标题颜色的规则中,我们使用变量所在的位置已被其值所替换。

列表 12.11 标题颜色 CSS 输出

@import url("https:/ /fonts.googleapis.com/css2
➥ ?family=Nunito:wght@300;400;500;800&display=swap");
body {
  font-family: "Nunito", sans-serif;
  font-weight: 300;
  max-width: 72ch;
  margin: 2rem auto;
}

p {
  line-height: 1.5;
}

h1, h2 {
  color: #063373;
}

/*# sourceMappingURL=styles.css.map */

现在我们的项目标题看起来像图 12.8。接下来让我们为我们的图片添加样式。

图 12.8 更新后的标题颜色

12.2.1 @extend

Sass 给我们带来了几个新的 at-rules,其中两个是 @extend@include。这些规则允许我们构建通用的类,我们可以在整个代码中重用它们。在 CSS 中重用类的一种方法是为单个规则创建多个选择器,就像我们在设置标题样式时做的那样。我们不是为每个标题(<h1><h2>)创建两个相同的规则,而是创建了一个规则,并给它两个选择器:h1, h2 { }

@extend 允许我们创建一个基础规则,稍后可以从不同的规则中指向它。然后选择器将被添加到基础规则的选择器列表中。让我们使用这项技术来设置我们的图片样式,并看看它是如何工作的。

首先,我们创建一个基础规则,它将定义图片的 heightwidthobject-fitmargin。因为我们有三个图片,而且我们希望给每个图片一个稍微不同的边框半径和定位,所以我们分别将每个图片指向我们的 base-image 规则。下面的列表显示了如何操作。

列表 12.12 扩展图片样式(SCSS)

.image-base {                                   ①
  width: 300px;                                 ①
  height: 300px;                                ①
  object-fit: cover;                            ①
  margin: 0 2rem;                               ①
}                                               ①

img:first-of-type { @extend .image-base; }      ②
img:nth-of-type(2) { @extend .image-base; }     ②
img:last-of-type { @extend .image-base; }       ②

① 基础规则

② 扩展基础规则的图片

下面的列表显示了 CSS 输出。

列表 12.13 扩展图片样式(CSS 输出)

.image-base, img:last-of-type, img:nth-of-type(2), img:first-of-type {
  width: 300px;
  height: 300px;
  object-fit: cover;
  margin: 0 2rem;
}

通过创建基础规则然后使用 @extend,我们可以创建一些默认值并将它们应用到任何其他选择器,而无需重复我们的 CSS 代码。我们还可以将所有与选择器相关的代码放在一个规则中。在我们的默认图片样式应用后(图 12.9),让我们分别自定义它们。

图片 12-09

图 12.9 基础图片样式

12.3 @mixin 和 @include

我们想自定义每个图片的 border-radiuspositionobject-position。为此,我们将使用一个 mixin。Mixins 允许我们生成声明和规则。像函数一样,它们接受参数(尽管这不是强制的)并返回样式。让我们写一个将返回每个图片三个声明的 mixin。mixin 是一个 at-rule,因此它以 @mixin 开头,后面跟着我们想要给它的名字。接下来,我们添加括号,其中包含我们想要传递的任何参数。最后,我们添加一组大括号,在其中定义 mixin 想要返回的样式。图 12.10 显示了语法。

图片 12-10

图 12.10 混合语法

注意到每个参数都以美元符号开头。在 Sass 中,参数的名称定义方式与以 $ 开头的变量相同。

在 mixin 中,我们将这些参数值分配给属性,如下列 12.14 所示。我们改变边框半径,浮动图片,并移除它浮动的侧面的边距。请注意,mixin 需要在使用之前定义,因此通常将 mixins 放在文件的开始部分。

列表 12.14 构建 mixin(SCSS)

@mixin handle-img($border-radius, $position, $side) {
  border-radius: $border-radius;
  object-position: $position;
  float: $side;
  margin-#{$side}: 0;    ①
}

① 插值(第 12.3.2 节)

到目前为止,我们没有在项目中看到任何变化。我们已经定义了 mixin,但还没有使用它。在我们应用它之前,让我们更仔细地看看它的某些属性。

12.3.1 object-fit 属性

在我们的基规则中,我们将object-fit属性值设置为coverobject-position属性,我们也在 mixin 中使用,与object-fit协同工作,确定图像在其边界框内的对齐方式。记住,cover使浏览器根据提供的维度计算图像的最佳大小,以便尽可能多的图像在不失真的情况下显示。

如果提供给图像的维度与图像的宽高比不同,则超出部分将被裁剪。object-position属性改变图像在容器内的位置,允许我们在比例不匹配时操作图像被裁剪的部分(图 12.11)。

图片 12-11

图 12.11 使用object-positionobject-fit: cover结合时图像的可见部分与裁剪部分

12.3.2 插值

注意边距的语法:margin-#{$side}: 0;。我们在变量周围添加了哈希(#)和大括号。这种语法,称为插值,允许我们将值插入到我们的参数中。它将表达式的结果嵌入到我们的 CSS 中的大括号内,替换掉哈希。例如,如果$side的值等于"left",我们的声明将编译为margin-left: 0;。

你可能在 JavaScript 的模板字面量字符串插值上下文中遇到过插值:`margin-${side}`。在我们的项目中,我们试图连接margin-$side变量的值。因为'margin-' + $side不是一个有效的属性声明,所以我们使用插值来插入值。

12.3.3 使用 mixin

接下来,我们将使用我们的 mixin 在每个图像规则中。为此,我们使用@include后跟 mixin 的名称,并在括号中包含它所需的参数(图 12.12)。

图片 12-12

图 12.12 @mixin 语法

在所有三个图像规则中,我们使用@include handle-img()并传入我们想要使用的border-radiusobject-positionfloat属性值(列表 12.15)。所有三个图像都有圆角(mixin 的第一个参数)。我们的第一和第二个图像使用border-radius缩写属性,我们将在 12.3.4 节中讨论。

列表 12.15 使用 mixin(SCSS)

@mixin handle-img($border-radius, $position, $side) {
  border-radius: $border-radius;
  object-position: $position;
  float: $side;
  margin-#{$side}: 0;
}

img:first-of-type {
  @extend .image-base;
  @include handle-img(20px 100px 10px 20px, center, left);
}

img:nth-of-type(2) {
  @extend .image-base;
  @include handle-img(100px 20px 10px 20px, left top, right);
}

img:last-of-type {
  @extend .image-base;
  @include handle-img(50px, center, left);
}

在我们的输出 CSS 中,mixin 本身并不存在,但我们有三个新的规则,每个图像一个,如下所示。

列表 12.16 使用 mixin 输出(CSS)

.image-base, img:last-of-type, img:nth-of-type(2), img:first-of-type {   ①
  width: 300px;                                                          ①
  height: 300px;                                                         ①
  object-fit: cover;                                                     ①
  margin: 0 2rem;                                                        ①
}                                                                        ①

img:first-of-type {                                                      ②
  border-radius: 20px 100px 10px 20px;                                   ②
  object-position: center;                                               ②
  float: left;                                                           ②
  margin-left: 0;                                                        ②
}                                                                        ②

img:nth-of-type(2) {                                                     ②
  border-radius: 100px 20px 10px 20px;                                   ②
  object-position: left top;                                             ②
  float: right;                                                          ②
  margin-right: 0;                                                       ②
}                                                                        ②
img:last-of-type {                                                       ②
  border-radius: 50px;                                                   ②
  object-position: center;                                               ②
  float: left;                                                           ②
  margin-left: 0;                                                        ②
}                                                                        ②

① 通过使用@extend 添加到基类中的选择器

② 通过使用 mixin 生成(@include)

此输出揭示了使用 @extend 和使用混合(@include)之间的区别。当我们扩展一个规则时,Sass 不会复制或生成代码;它只是将选择器添加到基础中。当我们使用混合时,Sass 会生成代码。如果我们正在动态设置属性,我们想要使用混合。但如果属性值是静态的,我们想要扩展;否则,我们每次使用混合时都会复制这些值,这会使我们的样式表膨胀。在此阶段,我们的项目看起来像图 12.13。

图片

图 12.13 样式化图片

12.3.4 border-radius 简写

对于我们的第一张和第二张图片,我们使用了 border-radius 简写。第一张图片生成的 CSS 中 border-radius 属性的值为 20px 100px 10px 20px。正如我们可以在一个声明中为元素的所有四个边设置不同的填充值一样,border-radius 允许我们使用类似的语法(图 12.14)。每个值定义了从左上角开始并顺时针旋转的角落半径。

图片

图 12.14 border-radius 属性

现在我们已经对图片进行了样式化,让我们更仔细地看看我们的文本。在一些段落中,我们有链接需要样式化。

12.4 嵌套

Sass 让我们能够做到的一件酷事是嵌套规则。当我们样式化链接时,我们经常编写多个规则,以便我们可以处理各种状态(链接、已访问、悬停、聚焦等)。我们可以像列表 12.17 所示那样将它们嵌套在一起。嵌套我们的规则清楚地显示了代码中的祖先-后代关系,并使我们的规则分组和组织。

要选择父选择器,我们使用一个和号(&)。在我们的规则中,父规则是针对锚点元素的。在这个规则内部,我们需要引用父元素(a)以与 :link:visited:hover:focus 伪类一起使用,因此我们在它们之前加上 &

我们使所有锚点元素加粗,通过使用我们的 $primary 变量使它们变为蓝色,并将我们的链接下划线从实线改为点线。在悬停时,我们将下划线变为虚线。最后,我们将聚焦下划线变为实线。在聚焦时,我们还移除了某些浏览器中存在的默认轮廓。

列表 12.17 嵌套规则(SCSS)

a {                                             ①
  font-weight: 800;
  &:link, &:visited {                           ②
    color: $primary;
    text-decoration-style: dotted;              ③
  }
  &:hover { text-decoration-style: dashed;}     ④
  &:focus {                                     ⑤
    text-decoration-style: solid;               ⑥
    outline: none;
  }
}

① 所有锚点元素:父元素

② 包含 href 的锚点元素,无论是已访问的还是未访问的

③ 将下划线样式改为点线

④ 链接悬停时,将下划线样式改为虚线

⑤ 链接聚焦时

⑥ 将下划线样式改为实线

在我们的 CSS 输出中,如下所示列表中所示,我们的嵌套规则已被展平,为锚点元素及其每个状态创建了单独的规则。现在我们的链接看起来像图 12.15。

列表 12.18 嵌套规则(CSS 输出)

a {
  font-weight: 800;
}
a:link, a:visited {
  color: #063373;
  text-decoration-style: dotted;
}
a:hover {
  text-decoration-style: dashed;
}
a:focus {
  text-decoration-style: solid;
  outline: none;
}

图片

图 12.15 样式化链接:(从上到下)默认、悬停和聚焦

注意嵌套是一种很好的方式来保持我们的规则分组和组织。但是,对于每一层嵌套,都有另一个具体性级别。在列表 12.17 中,我们在锚点(a)规则内部嵌套了悬停和聚焦。输出(列表 12.18)中的内部选择器比外部规则更具体:a:hover 比较具体于 a。通过嵌套规则,我们很容易创建过于具体的规则,这会降低性能。我们需要在我们的代码中留意过度嵌套。如果我们注意到嵌套超过三个层级,我们应该检查我们的规则是如何嵌套的,并看看是否有一些规则可以被解嵌套。

链接样式完成后,我们接下来要关注的是调用段落。

12.5 @each

在我们的文本中,我们有三个具有 successwarningerror 类的调用段落。正如我们在样式化图像时所做的(第 12.4 节),我们将创建一个基础规则,然后扩展它(列表 12.19)。该规则定义了我们想要调用具有的 borderborder-radiuspadding,并且它包括所有三种类型共有的样式。

列表 12.19 调用基础规则

.callout {
  border: solid 1px;
  border-radius: 4px;
  padding: .5rem 1rem;
}

接下来,我们不再为每个调用类型编写单独的规则,而是创建一个映射,这是一个键值对的列表,我们可以遍历它来生成规则集。因为我们的调用区分因素是颜色,所以我们的键将是类型,我们的值将是我们在本章开头定义的颜色变量。因此,我们的映射将是 $callouts: (success: $success, warning: $warning, error: $error);。图 12.16 分解了语法。

图 12.16 Sass 映射语法

创建了映射后,我们可以遍历每个键值对来生成我们的类。对于循环,我们将使用 @each。这个 at 规则按顺序遍历列表或映射中的所有项,这对于我们的用例来说非常完美。我们将在我们的 SCSS 中添加以下规则:@each $type, $color in $callouts {}。第一个变量($type)给我们提供了访问键的权限,第二个($color)是键对的值,最后一个($callouts)是我们想要遍历的映射。我们将生成规则的代码放在大括号内。为了测试我们的循环,我们可以在大括号内添加一个 @debug 声明来检查我们的变量值是否符合预期(列表 12.20)。

注意 @debug 是 Sass 中 JavaScript 的 console.log() 的等价物。它允许我们将值打印到终端。不幸的是,CodePen 似乎没有一种方法可以在其控制台中暴露 Sass 调试语句。这些语句也不会在浏览器控制台中显示。你只能在本地运行项目时看到调试输出。

列表 12.20 循环中的 @debug 语句(SCSS)

$callouts: (success: $success, warning: $warning, error: $error);    ①
@each $type, $color in $callouts {                                   ②
  @debug $type, $color;                                              ③
}

① 映射

② 设置循环

③ 将打印我们的 $type 和 $color 值到终端的调试语句

在我们运行 Sass 监视器的终端中,@debug语句输出了文件名、行号、单词调试以及我们两个变量的值(列表 12.21)。请注意,您的行号可能与列表中显示的略有不同。

列表 12.21 终端输出

before/styles.scss:70 Debug: success, #747d10       ①
before/styles.scss:70 Debug: warning, #fc9d03       ②
before/styles.scss:70 Debug: error, #940a0a         ③
Compiled before/styles.scss to before/styles.css.

① 第一个键值对

② 第二个键值对

③ 第三个键值对

现在我们知道我们的循环工作正常,我们可以为我们的提示框类型创建规则。在每个规则集中,我们扩展.callout基本规则,并使用border-color添加每个类型的正确边框颜色。border-color属性的值来自我们的@each循环的$color变量。我们之前提到 Sass 变量是静态的(第 12.2 节)。因此,$color变量的值在映射中的每个键值对中都会重新分配,为每个提示框类型正确分配border-color

接下来,我们通过使用::before伪元素在段落之前添加类型名称,以便我们有一个除了颜色之外的可视指示器,告诉用户提示框的类型是什么。因为我们的映射中的类型值是小写的,所以我们还使用text-transform将其转换为大写。列表 12.22 显示了我们的更新后的循环。

注意:永远不要仅使用颜色来传达意义。一些用户,如色盲用户,可能难以感知颜色,甚至可能完全看不到它们。在我们的例子中,颜色传达了提示框的类型,因此我们应该包含一些其他指示器(文本)。

列表 12.22 向循环中添加(SCSS)

.callout {
  border: solid 1px;
  border-radius: 4px;
  padding: .5rem 1rem;
}

$callouts: (success: $success, warning: $warning, error: $error);
@each $type, $color in $callouts {
 @debug $type, $color;
  .#{$type} {                       ①
    @extend .callout;
    border-color: $color;
    &::before {
      content: "#{$type}: ";        ②
      text-transform: capitalize;
    }
  }
}

① 插值以创建类名

② 插值以获取内容中的类型名称

正如我们在 12.3.2 节中使用插值创建边距声明时所做的,我们在这里使用它来创建类名并将类型添加到内容中。通过遍历映射,我们的@each规则创建了三个规则,每个类型一个。每个选择器也通过@extend添加到.callout规则中,如下面的列表所示。

列表 12.23 循环 CSS 输出

.callout, .error, .warning, .success {     ①
  border: solid 1px;
  border-radius: 4px;
  padding: 0.5rem 1rem;
}

.success {
  border-color: #747d10;
}
.success::before {
  content: "success: ";
  text-transform: capitalize;
}

.warning {
  border-color: #fc9d03;
}
.warning::before {
  content: "warning: ";
  text-transform: capitalize;
}

.error {
  border-color: #940a0a;
}
.error::before {
  content: "error: ";
  text-transform: capitalize;
}

① 所有三个类选择器(.error, .warning, .success)都被添加到.callout 基本类中。

现在我们三个提示框都有彩色边框(图 12.17)。但我们仍然需要在错误提示框中加粗错误:并添加背景颜色。

图 12.17 包含彩色边框的提示框样式

12.6 颜色函数

我们希望每个提示框的背景颜色都要比我们目前存储在变量中的颜色明显浅很多。为了使颜色操作更简单,Sass 提供了用于操作颜色的函数。我们将使用scale-color()scale-color()函数非常灵活,可以用来改变颜色的红色、蓝色和绿色成分;改变饱和度或不透明度;以及使颜色变浅或变深(图 12.18)。

图 12.18 scale-color()函数

值得注意的是,scale-color() 使用 HSL(色调、饱和度和亮度)或 RGB(红色、绿色和蓝色)参数;它们不能混合使用。然而,alpha(透明度)参数可以与任何一组参数一起使用。此外,参数可以省略。因此,如果我们只想更改不透明度,我们只需要传递初始颜色和我们要用来操作颜色的参数(s)。

对于我们的背景,我们需要增加颜色的亮度,所以我们使用 HSL 参数。我们不需要更改饱和度,所以我们将省略饱和度参数,只传递颜色和我们要增加亮度的量(86%),如下所示。

列表 12.24 添加背景颜色(SCSS)

$callouts: (success: $success, warning: $warning, error: $error);
@each $type, $color in $callouts {
  @debug $type, $color;
  .#{$type} {
    @extend .callout;
    background-color: scale-color($color, $lightness: +86%);     ①
    border-color: $color;
    &::before {
      content: "#{$type}: ";
      text-transform: capitalize;
    }
  }
}

① 增加了映射中提供的颜色的亮度 86%

以下列表显示了 scale-color() 函数在 CSS 输出中生成的颜色。

列表 12.25 scale-color() 函数输出(CSS)

.callout, .error, .warning, .success {
  border: solid 1px;
  border-radius: 4px;
  padding: 0.5rem 1rem;
}

.success {
  background-color: #f6f9d1;
  border-color: #747d10;
}
.success::before {
  content: "success: ";
  text-transform: capitalize;
}

.warning {
  background-color: #fff1dc;
  border-color: #fc9d03;
}
.warning::before {
  content: "warning: ";
  text-transform: capitalize;
}

.error {
  background-color: #fcd1d1;
  border-color: #940a0a;
}
.error::before {
  content: "error: ";
  text-transform: capitalize;
}

现在我们已经添加了背景颜色(图 12.19),我们剩下要做的就是将 *Error:* 作为错误调用部分的 ::before 内容加粗。

12.7 @if 和 @else

由于 Sass 的存在,可用的另一组 at 规则是 @if@else,它们控制是否评估代码块,并在条件不满足时提供回退条件。我们将在循环中使用它们,如果调用类型是 error,则仅将 ::before 伪元素的文本加粗,对于其他类型则增加字体粗细到中等(500)。

如果你习惯了 JavaScript,Sass 中的相等性评估可能会让你遇到一些陷阱,因为 Sass 没有真/假行为。只有当值具有相同的值和类型时,才被认为是相等的。此外,Sass 不使用双竖线(||)或双与(&&),而是使用 orand 来考虑多个条件。以下列表显示了 Sass 的一些相等性运算符及其结果。

列表 12.26 等式(SCSS)

@debug '' == false;             // false    ①
@debug 'true' == true;          // false    ①
@debug null == false;           // false    ①
@debug Verdana == 'Verdana';    // true     ②
@debug 1cm == 10mm;             // true     ③
@debug 4 > 5 or 8 > 5;          // true
@debug 4 > 5 and 8 > 5;         // false

truefalsenull 只与自己相等。

② 两个值都被视为字符串。

③ 转换为相同的单位,它们在大小上相等;因此,它们相等。

为了检查我们的 $type 变量是否等于 'error',我们的条件将是 $type == 'error' 结合 @if@else。我们的规则如下所示。

列表 12.27 条件加粗调用类型(SCSS)

$callouts: (success: $success, warning: $warning, error: $error);
@each $type, $color in $callouts {
  @debug $type, $color;
  .#{$type} {
    @extend .callout;
    background-color: scale-color($color, $lightness: +86%);
    border-color: $color;
    &::before {
      content: "#{$type}: ";
      text-transform: capitalize;
      @if $type == 'error' {       ①
        font-weight: 800;          ①
      } @else {                    ②
        font-weight: 500;          ②
      }                            ②
    }
  }
}

① 类型是错误;因此,我们添加了 800 的字体宽度。

② 类型不是错误(它是成功或警告),因此字体粗细设置为 500。

以下列表显示,CSS 输出中已为每种类型添加了字体粗细。

列表 12.28 条件加粗调用类型(CSS 输出)

.callout, .error, .warning, .success {
  border: solid 1px;
  border-radius: 4px;
  padding: 0.5rem 1rem;
}

.success {
  background-color: #f6f9d1;
  border-color: #747d10;
}
.success::before {
  content: "success: ";
  text-transform: capitalize;
  font-weight: 500;
}

.warning {
  background-color: #fff1dc;
  border-color: #fc9d03;
}
.warning::before {
  content: "warning: ";
  text-transform: capitalize;
  font-weight: 500;
}

.error {
  background-color: #fcd1d1;
  border-color: #940a0a;
}
.error::before {
  content: "error: ";
  text-transform: capitalize;
  font-weight: 800;
}

作为 ::before 伪元素的文本添加部分,.success.warningfont-weight 都是 500。另一方面,.error::before 规则的 font-weight800

添加了这个最后细节后,我们的项目就完成了。图 12.19 展示了最终输出。

图 12.19 完成的项目

12.8 最后的想法

本章展示了 SaaS 允许我们做一些仅使用 CSS 无法做到的事情,但它只涵盖了 SaaS 功能的一小部分,并且只深入探讨了一个预处理器。预处理器可以做更多;本章只是触及了表面。重要的是要记住,预处理器提供了酷炫的功能,可以使代码编写更高效,同时也更复杂。它们还要求有一个构建步骤和稍微复杂一些的设置。

尽管我们没有深入探讨 Less 或 Stylus,以下是一些在选择预处理器时可能有助于你的问题:

  • 我是否需要预处理器?

  • 预处理器需要哪些功能?

  • 使用预处理器将如何帮助我的项目开发?

  • 如果项目使用用户界面框架或库,它是否支持一个或多个预处理器?如果是,哪些?

  • 由于现在 CSS 需要构建,拥有预处理器将如何改变我的构建和部署流程?

  • 我的团队成员有哪些技能,他们熟悉哪些预处理器?

无论预处理器是否适合你,重要的是要记住每个项目都是不同的。继续学习、探索和尝试新事物,并享受乐趣。编码愉快!

摘要

  • Sass 有两种语法:缩进和 SCSS。

  • 变量和 CSS 自定义属性的工作方式不同。

  • Sass 变量是块作用域的。

  • @extend 扩展现有规则,而混合生成新代码。

  • 混合可以接受参数。

  • 当与 object-fit: cover 结合使用时,object-position 帮助在图像没有与给定的尺寸相同的宽高比时,在边界框内定位图像。

  • 插值用于嵌入表达式的结果,例如在从变量创建规则名称时。

  • border-radius 属性可以接受多个值,以将不同的曲率分配给元素每个角落,从左上角开始,按顺时针方向旋转。

  • Sass 允许我们嵌套规则。

  • 我们可以使用 @each 来遍历列表和映射。

  • @debug 允许我们在终端输出中打印值。

  • Sass 提供了如 scale-color() 这样的函数来操纵和改变颜色。

  • @if@else 可以用来确定是否应该评估代码块。

附录。

与供应商前缀和功能标志一起工作

CSS 中最令人沮丧的方面之一,尤其是在使用新的 CSS 语法时,就是供应商前缀。每个浏览器都有一个类型的引擎,这被称为 供应商。引擎的目的是将代码(HTML、CSS 和 JavaScript)转换为最终用户看到的和与之交互的内容,例如网页或应用程序。主要有三个主要的浏览器引擎:

  • Gecko(也称为 Quantum)——由 Firefox 浏览器使用,并由 Mozilla 维护

  • WebKit——由 Safari 和 iOS Safari 使用,并由 Apple 开发

  • Blink——由 Chrome、Microsoft Edge 和 Opera 使用,并由 Google 维护

作为 CSS 的编写者,你可能会发现一些属性仍然需要供应商前缀,特别是如果你或你所在的组织支持旧浏览器引擎。前缀位于 CSS 属性之前。总共有四个前缀,列于表 A.1 中。

表 A.1 浏览器前缀列表

Prefix 浏览器
-webkit- Android, Chrome, iOS, Edge 和 Opera(较新版本),以及 Safari
-ms- Internet Explorer 和 Edge(较旧版本)
-o- Opera(较旧版本)
-moz- Firefox

尽管 Chrome 使用 Blink 引擎,但它仍然使用 -webkit- 前缀,因为 Chrome 是基于 WebKit 构建的。当 Chrome 转移到 Blink 引擎时,它决定继续使用 -webkit- 前缀而不是创建一个新的前缀,以减少混淆。正如你将在本附录中看到的那样,无论如何,正在逐渐放弃使用前缀。

当使用前缀时,你应该将前缀版本放在非前缀版本之前。包含非前缀版本的原因是,当浏览器完全支持该属性时,它将使用非前缀版本;然后你可以移除前缀。一个需要前缀的 CSS 属性示例是 user-select,如果你使用 none 作为值:

.prevent-selecting{
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

供应商前缀背后的想法是,你可以在新的 CSS 在浏览器之间标准化时尝试新的 CSS,而不会破坏用户体验。然而,我们不推荐将带有前缀的代码直接提供给用户,因为浏览器解释该代码的方式可能会改变。

由于浏览器前缀导致了部分实现和错误,并且长期以来让开发者感到困惑,因此正在逐渐放弃使用浏览器前缀。我们经常看到一些样式表带有前缀,而这些前缀已经好几年没有使用了,因为样式表没有更新,或者开发者不确定是否安全地移除前缀。相反,现在正转向使用功能标志,用户可以控制这些标志。当编写 CSS 时,你会发现一些仍在使用的 CSS 属性需要浏览器前缀。在这种情况下,CSS 属性的前缀版本应该放在非前缀版本之前。

使用浏览器开发者工具

Chrome、Safari、Firefox 和其他主流浏览器都提供了开发者工具,非常适合编辑和诊断问题,尤其是如果您正在进行前端开发。您可以在浏览器内编辑您的 CSS,然后将样式复制并粘贴到您的项目中。

工具及其呈现方式在不同浏览器中有所不同。以下是主要浏览器中普遍存在的一些有用功能:

  • 元素面板,在这里您可以查看和更改文档对象模型(DOM)和 CSS。

  • 控制台面板,它突出显示加载资源(如 CSS、图片和其他媒体项)时出现的任何错误。

  • 网络和性能面板,这些在不同浏览器中可能会有所不同。在 Chrome 中,您可以使用这些面板查看网页的加载情况,并找到提高页面性能和效率的机会。

每个浏览器都有自己的开发者工具文档,这些资料在您学习 CSS 知识时值得探索(表 A.2)。

表 A.2 浏览器开发者工具文档

浏览器 URL
Chrome mng.bz/N2d2
Firefox mng.bz/D489
Safari developer.apple.com/safari/tools
Edge mng.bz/lWEM
posted @ 2025-11-24 09:12  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报