CSS-权威指南第五版-全-

CSS 权威指南第五版(全)

原文:zh.annas-archive.org/md5/afdc7f7c15196bd9b8e7f33e55ba3340

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

如果您是一位对精细页面样式、提升可访问性并节省时间和精力感兴趣的网页设计师或文档作者,这本书适合您。开始阅读这本书前,您真正需要了解的只有 HTML 4.0. 您对 HTML 了解得越多,您准备得越好,但这不是必需条件。要跟随本书,您只需了解很少其他内容。

该书的第五版完成于 2022 年底,并尽力反映当时的 CSS 状态。任何在写作时具有广泛浏览器支持或已知即将在出版后不久到来的内容都有详细介绍。仍在开发中或已知支持即将停止的 CSS 特性在此未涵盖。

本书中使用的约定

本书使用以下排印约定(但务必阅读“值语法约定”以查看其中一些的修改方式):

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

固定宽度

用于程序清单,以及段落内指向程序元素如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

常量宽度斜体

显示应由用户提供的值或由上下文确定的值的文本。

提示

此元素表示提示或建议。

此元素表示一般注释。

警告

此元素表示警告或注意事项。

值语法约定

本书中的各个 CSS 属性的详细信息都在方框中解释,包括允许使用的值。这些内容基本上是从 CSS 规范中实质性复制的,但需要解释语法。

每个属性的允许值将以以下语法列出:

值: <family-name>#

值: <url> ‖ <color>

值: <url>? <color> [ / <color> ]?

值: [ <length> | thick | thin ]{1,4}

值: [ <background>, ]* <background-color>

任何在 <> 之间的斜体字给出一个值类型或者对另一个属性值的引用。例如,属性 font 接受属于属性 font-family 的值。这用文本 <font-family> 表示。类似地,如果允许值类型如颜色,将用 <color> 表示。

所有以 固定宽度 呈现的单词都是必须按照字面意思出现的关键词。斜杠 (/) 和逗号 (,) 也必须字面使用。

值定义的组件可以通过多种方式组合:

  • 两个或更多关键词连在一起,中间只有空格分隔,意味着所有关键词必须按给定顺序出现。例如,help me 表示该属性必须按照这些关键词顺序出现。

  • 如果垂直线分隔备选项(X | Y),则任何一个选项必须出现,但仅限一个。例如,[ X | Y | Z ] 允许 XYZ 中的任何一个。

  • 一个垂直双竖线(XY)意味着XY或两者都必须出现,但它们可以以任意顺序出现。因此:XYX YY X 都是有效的解释。

  • 一个双与号(X && Y)意味着XY都必须出现,尽管它们可以以任意顺序出现。因此:X YY X 都是有效的解释。

  • 方括号([…​])用于将事物分组在一起。因此,[please ‖ help ‖ me] do this 表示 pleasehelpme 这三个词可以以任意顺序出现,但每个只能出现一次。而 do this 必须始终按照指定的顺序出现。以下是一些例子:please help me do thishelp me please do thisme please help do this

每个组件或括号组后面可能(或者可能不)跟随这些修饰符之一:

  • 星号()表示前面的值或括号组可以重复零次或多次。因此,bucket 表示单词 bucket 可以任意次数使用,包括零次。没有定义可以使用的最大次数。

  • 加号(+)表示前面的值或括号组至少重复一次。因此,mop+ 表示单词 mop 必须至少使用一次,可能更多次。

  • 井号(#),正式称为八角符号,表示前面的值或括号组以逗号分隔,可以重复一次或多次。因此,floor# 可以是 floorfloor, floor, floor,等等。这通常与括号组或值类型一起使用。

  • 问号(?)表示前面的值或括号组是可选的。例如,[pine tree]? 表示 pine tree 这个词组不一定需要使用(但如果使用了,必须按照顺序出现)。

  • 感叹号(!)表示前面的值或括号组是必需的,因此必须至少得到一个值,即使语法似乎表明不需要。例如,[ what? is? happening? ]! 必须至少出现其中一个被标记为可选的三个术语。

  • 大括号中的一对数字({M,N})表示前面的值或括号组至少重复 M 次,最多重复 N 次。例如,ha 表示单词 ha 可以出现一次、两次或三次。

以下是一些例子:

givemeliberty

三个词中至少要使用一个,并且它们可以以任意顺序使用。例如,give libertygive meliberty me givegive me liberty 都是有效的解释。

[ I | am ]? thewalrus

可以使用单词Iam,但不能同时使用,使用任何一个是可选的。此外,thewalrus或两者必须以任意顺序跟随。因此,您可以构建I the walrusam walrus theam theI walruswalrus the等等。

koo+ ka-choo

一个或多个koo必须跟随ka-choo。因此koo koo ka-chookoo koo koo ka-chookoo ka-choo都是合法的。koo的数量可能是无限的,尽管可能存在特定于实现的限制。

I really? [ love | hate ][ Microsoft | Firefox | Opera | Safari | Chrome ]

通用 Web 设计师的意见表达器。这可以解释为I love FirefoxI really love Microsoft等表达。可以使用从零到四个really,但它们不能用逗号分隔。您还可以在lovehate之间进行选择,这似乎确实像某种隐喻。

It’s a [ mad ]# world

这提供了将尽可能多个逗号分隔的mad放入其中的机会,至少需要一个mad。如果只有一个mad,则不添加逗号。因此:It’s a mad worldIt’s a mad, mad, mad, mad, mad world都是有效的结果。

[ [AlphaBakerCray], ]{2,3}和 Delphi

AlphaBakerDelta的两到三个必须跟随and Delphi。一个可能的结果是Cray, Alpha 和 Delphi。在这种情况下,逗号是因为其在嵌套括号组内的位置而放置的。(一些旧版本的 CSS 通过这种方式强制使用逗号分隔,而不是通过#修饰符。)

使用代码示例

每当您遇到看起来像的图标时,这意味着有相关的代码示例。实时示例可在https://meyerweb.github.io/csstdg5figs找到。如果您在连接互联网的设备上阅读本书,可以单击图标直接访问引用的代码示例的实时版本。

附加材料——用于生成本书几乎所有图示的 HTML、CSS 和图像文件可通过https://github.com/meyerweb/csstdg5figs下载。请务必阅读存储库的README.md文件,了解有关存储库内容的任何注意事项。

如果您有技术问题或使用代码示例时出现问题,请发送电子邮件至bookquestions@oreilly.com

本书旨在帮助您完成工作任务。通常情况下,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们获得许可。例如,编写一个使用本书多个代码块的程序不需要许可。销售或分发 O'Reilly 书籍中的示例代码需要获得许可。引用本书并引用示例代码回答问题不需要许可。将本书大量示例代码整合到产品文档中需要获得许可。

我们感谢,但通常不要求署名。署名通常包括标题,作者,出版商和 ISBN。例如:“CSS: The Definitive Guide by Eric A. Meyer and Estelle Weyl (O’Reilly). Copyright 2023 Eric A. Meyer and Estelle Weyl, 978-1-098-11761-0.”

如果您认为您对代码示例的使用超出了合理使用或上述许可的范围,请随时联系我们permissions@oreilly.com

O’Reilly 在线学习

40 多年来,O’Reilly Media已经为公司提供技术和商业培训,知识和见解,帮助其取得成功。

我们独特的专家和创新者网络通过书籍,文章和我们的在线学习平台分享他们的知识和专业知识。O'Reilly 的在线学习平台为您提供按需访问的实时培训课程,深入学习路径,交互式编码环境以及来自 O'Reilly 和其他 200 多个出版商的广泛文本和视频收藏。有关更多信息,请访问https://oreilly.com

如何联系我们

请将有关本书的评论和问题发送至出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • 加利福尼亚州塞巴斯托波尔 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们有一个专门的网页为本书列出勘误表,示例和任何额外信息。您可以访问此页面https://oreil.ly/css-the-definitive-guide-5e

发送电子邮件至bookquestions@oreilly.com以评论或询问有关本书的技术问题。

有关我们的书籍和课程的新闻和信息,请访问https://oreilly.com

在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media

在 Twitter 上关注我们:https://twitter.com/oreillymedia

观看我们的 YouTube 频道:https://youtube.com/oreillymedia

致谢

Eric Meyer

首先,我要感谢本版的所有技术审阅者,他们投入了时间和专业知识来发现我所有错误的地方,而且收获远远不及他们应得的回报。按照姓氏字母顺序排列:Ire Aderinokun、Rachel Andrew、Adam Argyle、Amelia Bellamy-Royds、Chen Hui Jing、Stephanie Eckles、Eva Ferreira、Mandy Michael、Schalk Neethling、Jason Pamental、Janelle Pizarro、Eric Portis、Miriam Suzanne、Lea Verou 和 Dan Wilson。任何错误都是我的错,而不是他们的。

我也要感谢所有过去版本的技术审阅者,这里不能一一列举,以及多年来帮助我理解各种 CSS 细节的人,这些人同样也太多了无法一一列出。如果你曾经向我解释过一些 CSS,请在下面的空白处写下你的名字:_______________________,我对你的感激之情无以言表。

感谢所有 CSS 工作组的成员,无论是过去还是现在,你们把一个令人惊叹的语言引向了令人惊叹的高度......即使你们的工作意味着我们在下一版书籍中将面临真正的生产困境,这本书已经在合理技术可管理的极限范围内。

感谢所有保持 Mozilla 开发者网络(MDN)运行和更新的人。

特别感谢所有在 Open Web Docs 做出贡献的优秀人士,以及邀请我担任你们领导委员会成员的机会。

感谢我的合著者 Estelle,感谢你为本书的所有贡献、专业知识和推动所做的努力。

感谢所有的朋友、同事、同僚、熟人和路人,你们给予我奇怪喜好和古怪举止的空间,以及理解、耐心和善意。

而且,我永远对我的家人无限的感激——我的妻子 Kat 和我的孩子们,Carolyn、Rebecca z’’l和 Joshua。你们是庇护我的家园,是我天空中的太阳,是我航向的星星。感谢你们教会我的一切。

俄亥俄州克利夫兰高地

December 4, 2022

Estelle Weyl

我要感谢所有为使 CSS 成为今天的样子付出努力的人,以及所有在技术领域促进多样性和包容性的人。

许多人在与浏览器供应商和开发者合作撰写 CSS 规范时不知疲倦地工作着。如果没有 CSS 工作组的成员们——无论是过去的、现在的还是未来的——我们就不会有规范,没有标准,也没有跨浏览器兼容性。我对每个 CSS 属性和值被添加或从规范中省略的思考过程感到敬畏。像 Tab Atkins、Elika Etimad、Dave Baron、Léonie Watson 和 Greg Whitworth 这样的人不仅致力于规范的制定,还花时间回答问题,并向广大 CSS 公众解释细微之处,特别是我。

我还要感谢所有那些,无论他们是否参与 CSS 工作组,都深入研究 CSS 特性并帮助翻译规范给我们其他人看的人,包括 Sarah Drasner、Val Head、Sara Souidan、Chris Coyier、Jen Simmons 和 Rachel Andrew。此外,我要感谢那些创造使所有 CSS 开发者生活更轻松的工具的人,特别是 Alexis Deveria 因创建和维护Can I Use 工具

我还感激所有为改善开发者社区各个领域的多样性和包容性而贡献时间和精力的人。是的,CSS 很棒。但与伟大的人们在一个伟大的社区中共事同样重要。

当我在 2007 年参加我的第一次技术会议时,演讲者中有 93%是男性,100%是白人。观众的性别多样性稍低,而种族多样性略高。我选择那个会议是因为相比大多数会议,它的阵容更为多样化:实际上有一个女性在其中。环顾四周,我知道事情需要改变,我意识到这是我需要做的事情。那时我没有意识到在接下来的 10 年里,我会遇到多少默默无闻的英雄,他们致力于改善科技行业和生活中各个领域的多样性和包容性。

有太多人默默无闻地、不知疲倦地工作,通常几乎没有人认可他们的贡献,我无法一一列举,但我想重点提到一些人。像 Erica Stanley 的 Women Who Code Atlanta,Carina Zona 的 Callback Women,以及 Jenn Mei Wu 的 Oakland Maker Space 这些人对我产生了多么积极的影响。像 The Last Mile、Black Girls Code、Girls Incorporated、Sisters Code 等群体,还有许多其他组织,激励我创建了一个Feeding the Diversity Pipeline list,以确保通向网页开发职业的道路不仅仅是为特权阶层开放的。

谢谢大家。感谢所有人。由于你们的努力,比起我 10 年前在那个会议上能想象到的,做得更多。

加利福尼亚州旧金山

2023 年 2 月 14 日

第一章:CSS 基础

层叠样式表CSS),一种强大的编程语言,可以转换文档或文档集的呈现方式,已经传播到 Web 的几乎每个角落,以及许多表面上非 Web 的环境。例如,嵌入式设备显示器通常使用 CSS 来设计其用户界面,许多 RSS 客户端允许您对提要和提要条目应用 CSS,一些即时消息客户端使用 CSS 来格式化聊天窗口。CSS 的语法甚至可以在 JavaScript(JS)框架及 JS 本身中找到。它无处不在!

(Web)样式的简要历史

CSS 最初于 1994 年提出,正当 Web 开始真正流行起来。那时,浏览器为用户提供了各种样式控制权——例如,NCSA Mosaic 中的展示偏好允许用户定义每个元素的字体系列、大小和颜色。但这些对文档作者并不可用;他们只能标记内容片段为段落、某个级别的标题、预格式化文本或其他十几种元素类型之一。如果用户配置他们的浏览器,使所有一级标题变小和粉红色,而所有六级标题变大和红色,那就是他们的事情了。

正是在这样的环境中引入了 CSS。它的目标是为 Web 页面作者提供一种简单的声明式样式语言,既灵活又能为作者和用户提供样式化能力。通过级联,这些样式可以结合和优先级,以便站点作者和读者都能发言——尽管读者始终有最后的发言权。

工作迅速推进,到 1996 年末,CSS1 完成。尽管 CSS 的每个部分单独来看都相当简单,但这些部分的组合却产生了一些令人惊讶的复杂行为。不幸的错误也发生了,比如盒模型实现中的臭名昭著的不一致。这些问题曾经威胁要完全破坏 CSS,但幸运的是一些聪明的提案得以实施,浏览器开始协调一致。几年后,由于不断增加的互操作性以及高调的开发,比如基于 CSS 的Wired杂志重设计和 CSS Zen Garden,CSS 开始流行起来。

在所有这些发生之前,CSS 工作组在 1998 年初完成了 CSS2 规范。一旦 CSS2 完成,CSS3 的工作立即开始,同时还有 CSS2 的澄清版本称为 CSS2.1。符合时代精神,最初被称为CSS3的内容被构建为一系列(理论上)独立的模块,而不是单一的整体规范。这种方法反映了当时活跃的 XHTML 规范,出于类似的原因被分割为模块。

将 CSS 模块化的理由是,每个模块可以按照自己的节奏进行工作,特别关键(或流行)的模块可以在世界范围内的万维网联盟(W3C)进展轨迹中推进,而不会被其他模块所阻碍。事实上,情况确实如此。到 2012 年初,三个 CSS Level 3 模块(以及 CSS1 和 CSS 2.1)已经达到了完整的推荐状态——CSS Color Level 3、CSS Namespaces 和 Selectors Level 3。与此同时,还有七个模块处于候选推荐状态,其他几十个模块则处于不同的工作草案阶段。按照旧的方法,颜色、选择器和命名空间必须等待规范的每个其他部分完成或被删减,才能成为完成规范的一部分。多亏了模块化,它们无需等待。

因此,虽然我们无法指出一个单一的文档并说:“这就是 CSS”,但我们可以按照它们所引入的模块名称来讨论其特性。模块的灵活性远远超过了它们有时会造成的语义尴尬。 (如果您想要类似单一的庞大规范,CSS 工作组每年都会发布“快照”文档。)

有了这个基础,我们准备开始理解 CSS。让我们从涵盖样式表中的基础知识开始。

样式表内容

在样式表中,您会发现许多 规则,看起来有点像这样:

h1 {color: maroon;}
body {background: yellow;}

这些样式构成了任何样式表的主体部分——简单或复杂,短或长。但是哪些部分是什么,它们代表了什么?

规则结构

为了更详细地说明规则的概念,让我们来分解结构。

每个 规则 由两个基本部分组成:选择器和声明块。声明块由一个或多个声明组成,每个声明由属性和值对组成。每个样式表由一系列这些规则组成。 图 1-1 显示了规则的各个部分。

css5 0101

图 1-1. 规则的结构

选择器 显示在规则的左侧,定义了将被选中进行样式化的文档部分。在 图 1-1 中,选中的是 <h1>(一级标题)元素。如果选择器是 p,那么所有 <p>(段落)元素都将被选中。

规则的右侧包含声明块,它由一个或多个声明组成。每个声明由 CSS 属性和该属性的组成。在图 1-1 中,声明块包含两个声明。第一个声明指定了该规则将使文档中的部分以colorred显示,第二个声明指定了文档的一部分将以backgroundyellow。因此,文档中所有的<h1>元素(由选择器定义)将以红色文字和黄色背景样式显示。

供应商前缀

有时你会看到 CSS 片段的前面有连字符和标签,比如-o-border-image。这些供应商前缀是浏览器供应商标记属性、值或其他 CSS 部分为实验性或专有(或两者兼有)的一种方式。截至 2023 年初,一些供应商前缀仍在使用中,其中最常见的如表 1-1 所示。

表 1-1. 一些常见的供应商前缀

前缀 供应商
-epub- 国际数字出版论坛 EPUB 格式
-moz- 基于 Gecko 的浏览器(例如 Mozilla Firefox)
-ms- 微软 Internet Explorer
-o- 基于 Opera 的浏览器
-webkit- 基于 WebKit 的浏览器(例如 Apple Safari 和 Google Chrome)

如表 1-1 所示,供应商前缀的通常接受格式是一个连字符、一个标签和一个连字符,尽管一些前缀错误地省略了第一个连字符。

供应商前缀的使用和滥用非常复杂,超出了本书的范围。简单地说,它们最初是供应商测试新功能的一种方式,有助于加速互操作性,而不必担心被锁定在与其他浏览器不兼容的遗留行为中。这避免了几乎扼杀 CSS 在初期阶段的整个问题类别。不幸的是,带前缀的属性随后被 Web 作者公开部署,最终导致了一整套新问题。

截至 2023 年初,供应商前缀的 CSS 特性几乎不存在,旧的带前缀的属性和值正在被浏览器逐步移除。你很可能永远不会编写带前缀的 CSS,但你可能会在现有代码库中遇到它或者继承它。这里有一个例子:

-webkit-transform-origin: 0 0;
-moz-transform-origin: 0 0;
-o-transform-origin: 0 0;
transform-origin: 0 0;

这句话重复了四次:分别针对 WebKit、Gecko(Firefox)和 Opera 浏览器行,最后是 CSS 标准的方式。再次强调,这已经不再必要。我们在这里只是为了让你对将来可能遇到的情况有所了解。

空白处理

CSS 基本上不敏感于规则之间的空白,而且在规则内部的空白也大多不敏感,尽管存在一些例外。

通常情况下,CSS 对待空白符的方式与 HTML 相同:任何空白字符序列都会被解析为单个空格。因此,你可以按以下方式格式化这个假设的 rainbow 规则,

rainbow: infrared  red  orange  yellow  green  blue  indigo  violet  ultraviolet;

rainbow:
   infrared  red  orange  yellow  green  blue  indigo  violet  ultraviolet;

rainbow:
   infrared
   red
   orange
   yellow
   green
   blue
   indigo
   violet
   ultraviolet
   ;

以及你可以想象到的任何其他分隔模式。唯一的限制是分隔字符必须是空白符:一个空格,一个制表符或一个换行符,单独或组合,任意数量。

类似地,你可以以任何喜欢的方式使用空白符格式化一系列规则。这只是无数可能性中的五个示例之一:

html{color:black;}
body {background: white;}
p {
  color: gray;}
h2 {
     color : silver ;
   }
ol
   {
      color
         :
      silver
         ;
}

如你从第一条规则中看到的,空白符可以大大省略。事实上,在最小化的 CSS 中通常都是这种情况,即通过某种自动化的服务器端脚本删除了每一个可能多余的空白。在前两条规则之后的规则中,使用越来越多的空白,直到在最后一条规则中,几乎所有可以分离到自己一行的内容都已经这样做。

所有这些方法都是有效的,因此你应该选择最合乎逻辑——即在你看来最容易阅读的格式,并坚持使用它。

CSS 注释

CSS 允许注释。这些与 C/C++ 中的注释非常相似,因为它们被 /**/ 包围:

/* This is a CSS comment */

注释可以跨多行,就像在 C++ 中一样:

/* This is a CSS comment, and it
can be several lines long without
any problem whatsoever. */

重要的是要记住,CSS 注释不能嵌套。因此,例如,这种方式是不正确的:

/* This is a comment, in which we find
 another comment, which is WRONG
 /* Another comment */
 and back to the first comment, which is not a comment.*/
警告

如果在样式表中临时注释掉一个已包含注释的大块样式表,就可能意外创建“嵌套”注释。由于 CSS 不允许嵌套注释,“外部”注释将在“内部”注释结束处结束。

不幸的是,CSS 中没有像 //#(后者保留给 ID 选择器)那样的“行末注释”模式。CSS 中唯一的注释模式是 /* */。因此,如果你希望在标记的同一行上放置注释,你需要小心如何放置它们。例如,这是正确的方式:

h1 {color: gray;}   /* This CSS comment is several lines */
h2 {color: silver;} /* long, but since it is alongside */
p {color: white;}   /* actual styles, each line needs to */
pre {color: gray;}  /* be wrapped in comment markers. */

根据此示例,如果每行都没有被标记,样式表的大部分将成为注释的一部分,因此将无法起作用:

h1 {color: gray;}   /* This CSS comment is several lines
h2 {color: silver;}  long, but since it is not wrapped
p {color: white;}    in comment markers, the last three
pre {color: gray;}   styles are part of the comment. */

在这个示例中,只有第一条规则(h1 {color: gray;})将应用于文档。其余的规则作为注释的一部分将被浏览器的渲染引擎忽略。

注意

CSS 解析器将 CSS 注释视为根本不存在,因此在解析目的上不计入空白符。这意味着你可以将它们放在规则的中间——甚至直接放在声明内部!

标记

在样式表中没有标记。这似乎是显而易见的,但你可能会感到惊讶。唯一的例外是 HTML 注释标记,出于历史原因允许在 <style> 元素内部使用:

<style><!--
h1 {color: maroon;}
body {background: yellow;}
--></style>

就是这样,甚至这样也不再推荐;那些需要它的浏览器已经几乎消失了。

谈到标记,现在是时候稍微偏离一下,讨论我们的 CSS 将用于样式化的元素,以及这些元素如何受 CSS 在最基本的方式影响。

元素

元素 是文档结构的基础。在 HTML 中,最常见的元素很容易识别,例如 <p><table><span><a><article>。文档中的每个元素都在其呈现中发挥作用。

替换元素和非替换元素

尽管 CSS 依赖于元素,但并非所有元素都是平等的。例如,图片和段落不是同一类型的元素。在 CSS 中,元素通常有两种形式:替换和非替换。

替换元素

替换元素 用于指示将由文档本身不直接表示的内容替换的内容。可能最常见的 HTML 示例是 <img> 元素,它将替换为文档本身外部的图像文件。事实上, <img> 没有实际的内容,正如您可以在这个简单的示例中看到的:

<img src="howdy.gif" alt="Hello, friend!">

此标记片段仅包含一个元素名称和一个属性。除非您将其指向外部内容(在本例中,由src属性给出位置的图像文件),否则该元素不显示任何内容。如果指向有效的图像文件,则图像将显示在文档中。如果不是,则浏览器将显示空白或显示“损坏图像”占位符。

同样,input元素也可以被替换—可以是单选按钮、复选框、文本输入框或其他,具体取决于其类型。

非替换元素

大多数 HTML 元素都是非替换元素。它们的内容由用户代理(通常是浏览器)在元素本身生成的框内呈现。例如,<span>hi there</span>是一个非替换元素,文本“hi there”将由用户代理显示。这适用于段落、标题、表格单元、列表和几乎 HTML 中的所有其他元素。

元素显示角色

CSS 有两种基本的显示角色:块格式化上下文行内格式化上下文。还有许多其他显示类型存在,但这些是最基本的,大多数情况下,其他显示类型都与它们相关。块和行内上下文对于那些熟悉 HTML 标记及其在 Web 浏览器中显示的作者来说应该是很熟悉的。显示角色在图 1-2 中有所说明。

css5 0102

图 1-2. HTML 文档中的块级和行内级元素

块级元素

默认情况下,块级元素生成一个元素框,(默认情况下)填充其父元素的内容区域,并且不能在其两侧有其他元素。换句话说,在元素框前后会生成“断点”。来自 HTML 的最常见的块级元素是 <p><div>。替换元素可以是块级元素,但通常不是。

在 CSS 中,这被称为元素生成一个块级格式化上下文。这也意味着该元素生成一个块级外部显示类型。元素内部的部分可能具有不同的显示类型。

内联级别元素

默认情况下,内联级别元素在文本行中生成一个元素框,并且不会打断该行的流程。最佳的内联元素示例是 HTML 中的<a>元素。其他候选元素包括<strong><em>。这些元素在自身之前或之后不生成“断行”,因此它们可以出现在另一个元素的内容中,而不会破坏其显示。

在 CSS 中,这被称为元素生成一个内联格式化上下文。这也意味着该元素生成一个内联外部显示类型。元素内部的部分可能具有不同的显示类型。(在 CSS 中,对于显示角色如何嵌套在彼此中没有限制。)

为了看看它是如何工作的,让我们考虑 CSS 属性display

你可能已经注意到这里有很多值,其中只提到了两个:blockinline。这些值中的大多数在本书的其他地方有详细介绍;例如,gridinline-grid在第十二章中有讲述,而与表格相关的值则都在第十三章中有详细介绍。

现在,让我们集中讨论blockinline。考虑以下标记:

<body>
<p>This is a paragraph with <em>an inline element</em> within it.</p>
</body>

这里我们有两个元素(<body><p>),它们生成块级格式化上下文,还有一个元素(<em>)生成内联格式化上下文。根据 HTML 规范,<em>可以是<p>的后代,但反过来不成立。通常,HTML 层次结构使内联元素从块级元素派生,但反之则不然。

另一方面,CSS 则没有这样的限制。您可以保留标记不变,但更改两个元素的显示角色如下:

p {display: inline;}
em {display: block;}

这导致元素在内联框内生成一个块框。这是完全合法的,不违反 CSS 的任何部分。

虽然改变元素的显示角色在 HTML 文档中可能很有用,但对于 XML 文档而言,这变得非常关键。XML 文档不太可能具有任何固有的显示角色,因此由作者定义这些角色。例如,您可能想知道如何布置以下 XML 片段:

<book>
 <maintitle>The Victorian Internet</maintitle>
 <subtitle>The Remarkable Story of the Telegraph and the Nineteenth Century's
   On-Line Pioneers</subtitle>
 <author>Tom Standage</author>
 <publisher>Bloomsbury Pub Plc USA</publisher>
 <pubdate>February 25, 2014</pubdate>
 <isbn type="isbn-13">9781620405925</isbn>
 <isbn type="isbn-10">162040592X</isbn>
</book>

由于display的默认值是inline,因此内容将按默认情况下呈现为内联文本,如图 1-3 所示。这并不是一个非常有用的显示。

css5 0103

图 1-3. XML 文档的默认显示

您可以通过display来定义布局的基础:

book, maintitle, subtitle, author, isbn {display: block;}
publisher, pubdate {display: inline;}

现在我们将七个元素中的五个设置为块级,两个设置为内联。这意味着每个块级元素将生成自己的块级格式化上下文,而两个内联元素将生成自己的内联格式化上下文。

我们可以将前述规则作为起点,添加一些其他样式以获得更大的视觉冲击,并得到 图 1-4 中显示的结果。

css5 0104

图 1-4. XML 文档的样式化显示

话虽如此,在详细学习如何编写 CSS 之前,我们需要看看如何将 CSS 与文档关联起来。毕竟,如果没有将两者联系起来,CSS 就无法影响文档。我们将在 HTML 设置中探讨这一点,因为这是最熟悉的。

将 CSS 和 HTML 结合起来

我们已经提到 HTML 文档具有固有的结构,这是值得重申的一点。事实上,这也是旧网页的问题之一:我们中的许多人忘记了文档应该具有内部结构,这与视觉结构完全不同。在我们急于创建网上最酷的页面时,我们扭曲了和忽视了页面应该包含带有某些结构含义的信息的概念。

这种结构是 HTML 和 CSS 之间关系的固有部分;如果没有它,就根本不可能有关系。为了更好地理解它,让我们看一个例子 HTML 文档,并逐步分解它:

<!DOCTYPE html>
<html lang="en-us">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Eric's World of Waffles</title>
  <link rel="stylesheet" media="screen, print" href="sheet1.css">
  <style>
    /* These are my styles! Yay! */
    @import url(sheet2.css);
  </style>
</head>
<body>
  <h1>Waffles!</h1>
  <p style="color: gray;">The most wonderful of all breakfast foods is
  the waffle—a ridged and cratered slab of home-cooked, fluffy goodness
  that makes every child's heart soar with joy. And they're so easy to make!
  Just a simple waffle-maker and some batter, and you're ready for a morning
  of aromatic ecstasy!
  </p>
</body>
</html>

图 1-5 显示了此标记的结果和应用的样式。

css5 0105

图 1-5. 一个简单的文档

现在,让我们来看看这个文档连接到 CSS 的各种方式。

首先,考虑 <link> 标签的使用:

<link rel="stylesheet" href="sheet1.css" media="screen, print">

<link> 标签的基本目的是允许 HTML 作者将其他文档与包含 <link> 标签的文档关联起来。CSS 使用它将样式表链接到文档中。

这些样式表不是 HTML 文档的一部分,但仍然被其使用,称为 外部样式表。这是因为它们是外部于 HTML 文档的样式表。(看看这个。)

要成功加载外部样式表,应将 <link> 放置在 <head> 元素内,尽管它也可以出现在 <body> 元素内。这将导致 Web 浏览器定位并加载样式表,并使用它包含的任何样式来呈现 HTML 文档;图 1-6 描述了称为 sheet1.css 的样式表链接到文档的情况。

同时在 图 1-6 中显示了通过 @import 声明加载外部样式表 sheet2.css。导入必须放在包含它们的样式表的开头。

css5 0106

图 1-6. 外部样式表如何应用于文档的表示

外部样式表的格式是什么?它是一系列规则的列表,就像你在前一节和示例 HTML 文档中看到的那样;但在这种情况下,这些规则保存在它们自己的文件中。只要记住在样式表中不能包含任何 HTML 或其他标记语言,只能包含样式规则。以下是一个外部样式表的内容:

h1 {color: red;}
h2 {color: maroon; background-color: white;}
h3 {color: white; background-color: black;
  font: medium Helvetica;}

就是这样——没有任何 HTML 标记或注释,只有简单明了的样式声明。这些保存在一个纯文本文件中,通常扩展名为.css,例如sheet1.css

警告

外部样式表根本不能包含任何文档标记,只能包含 CSS 规则和 CSS 注释。在外部样式表中包含标记可能会导致其中的某些或所有内容被忽略。

属性

对于余下的<link>标签,属性和值都相当简单明了。rel属性代表关系,在这种情况下,关系是stylesheet。请注意,rel属性是必需的。CSS 有一个可选的type属性,默认值为text/css,因此您可以包含type="text/css"或者将其省略,取决于您的喜好。

这些属性值描述了使用<link>标签加载数据的关系和数据类型。这样,Web 浏览器知道样式表是 CSS 样式表,这一事实将决定浏览器如何处理导入的数据。(将来可能会使用其他样式语言。在这样的未来,如果您使用不同的样式语言,将需要声明type属性。)

接下来,我们找到href属性。此属性的值是您样式表的 URL。此 URL 可以是绝对的,也可以是相对的,即相对于包含该 URL 的文档的 URL,或者是一个指向网络上唯一位置的完整 URL。在我们的示例中,URL 是相对的。它也可以是绝对的,比如http://example.com/sheet1.css

最后,我们有一个media属性。此属性的值是一个或多个媒体描述符,这些描述符是关于媒体类型及其特性的规则,每个规则用逗号分隔。因此,例如,您可以在屏幕和打印媒体中使用链接样式表:

<link rel="stylesheet" href="visual-sheet.css" media="screen, print">

媒体描述符可能会变得非常复杂,在第二十一章中有详细解释。目前,我们将坚持显示的基本媒体类型。默认值是all,这意味着 CSS 将应用于所有媒体。

请注意,一个文档可以关联多个链接样式表。在这些情况下,只有relstylesheet<link>标签会在文档的初始显示中使用。因此,如果您想要链接两个名为basic.csssplash.css的样式表,它会像这样:

<link rel="stylesheet" href="basic.css">
<link rel="stylesheet" href="splash.css">

这将导致浏览器加载两个样式表,合并每个样式表中的规则,并将所有规则应用于所有媒体类型的文档(因为省略了media属性,将使用其默认值all)。例如:

<link rel="stylesheet" href="basic.css">
<link rel="stylesheet" href="splash.css">

<p class="a1">This paragraph will be gray only if styles from the
stylesheet 'basic.css' are applied.</p>
<p class="b1">This paragraph will be gray only if styles from the
stylesheet 'splash.css' are applied.</p>

此示例标记中缺少的一个属性是title。这个属性并不经常使用,但在未来可能变得重要,并且如果使用不当可能会产生意外的影响。为什么?我们将在下一节探讨这个问题。

备用样式表

可以在某些浏览器中定义备用样式表,用户可以选择。这些样式表的定义是通过将rel属性的值设置为alternate stylesheet,仅在用户选择时才用于文档呈现。

如果浏览器能够使用备用样式表,它将使用<link>元素的title属性值生成样式替代列表。因此,你可以写出以下内容:

<link rel="stylesheet" href="sheet1.css" title="Default">
<link rel="alternate stylesheet" href="bigtext.css" title="Big Text">
<link rel="alternate stylesheet" href="zany.css" title="Crazy colors!">

用户可以选择他们想要使用的样式,浏览器会从第一个样式表切换到用户选择的样式表,本例中标记为Default。 图 1-7 显示了实现这种选择机制的一种方式(实际上在 CSS 复兴早期就是这样)。

css5 0107

图 1-7. 提供备用样式表选择的浏览器
注意

早在 2023 年初,大多数基于 Gecko 的浏览器(如 Firefox)都支持备用样式表的选择。而 Chromium 和 WebKit 家族则不支持选择备用样式表。与图 1-7 中显示的浏览器构建日期(2002 年末)相比较。

还可以通过给它们相同的title值来将备用样式表分组在一起。因此,用户可以在屏幕和打印媒体上选择不同的站点呈现方式:

<link rel="stylesheet"
   href="sheet1.css" title="Default" media="screen">
<link rel="stylesheet"
   href="print-sheet1.css" title="Default" media="print">
<link rel="alternate stylesheet"
   href="bigtext.css" title="Big Text" media="screen">
<link rel="alternate stylesheet"
   href="print-bigtext.css" title="Big Text" media="print">

如果用户在符合条件的用户代理中从备用样式表选择机制中选择了大文本,bigtext.css将用于在屏幕媒体中为文档设置样式,而print-bigtext.css将用于打印媒体。sheet1.cssprint-sheet1.css将不会在任何媒体中使用。

为什么会这样?因为如果给一个具有relstylesheet<link>元素设置了标题,那么你就将这个样式表指定为首选样式表。在文档首次显示时,首选样式表会被优先使用。但是一旦选择了备用样式表,首选样式表将不会被使用。

此外,如果将多个样式表指定为首选样式表,则除一个之外的所有样式表都将被忽略。考虑以下代码示例:

<link rel="stylesheet"
   href="sheet1.css" title="Default Layout">
<link rel="stylesheet"
   href="sheet2.css" title="Default Text Sizes">
<link rel="stylesheet"
   href="sheet3.css" title="Default Colors">

现在,所有三个<link>元素都指向首选样式表,这要归功于所有三个元素上存在的title属性,但实际上只有一个元素会以这种方式被使用。其余两个将完全被忽略。哪两个会被忽略?无法确定,因为 HTML 没有提供一种方法来确定哪些首选样式表应该被忽略,哪些应该被使用。

如果未给样式表添加标题,它将成为持久样式表,始终在文档显示中使用。通常情况下,这正是作者想要的,尤其是因为备用样式表得不到广泛支持,几乎所有用户都不知晓其存在。

<style>元素

<style>元素是包含样式表的一种方式,它出现在文档本身中:

<style>...</style>

在开放和闭合的<style>标签之间的样式称为文档样式表嵌入样式表(因为这种样式表嵌入在文档中)。它包含适用于文档的样式,但也可以通过@import指令包含多个外部样式表的链接,这在下一节中讨论。

您可以给<style>元素添加一个media属性,其功能与链接样式表上的方式相同。例如,以下示例将限制嵌入样式表的规则仅在打印媒体中应用:

<style media="print">…</style>

您还可以使用<title>元素为嵌入样式表添加标签,方式与上一节关于备用样式表的讨论相同,出于同样的原因。

<link>元素类似,<style>元素可以使用type属性;对于 CSS 文档,正确的值是"text/css"。在 HTML 中,type属性是可选的,只要加载 CSS 即可,因为<style>元素的type属性的默认值是text/css。只有在使用其他样式语言时才需要显式声明type值,或许在未来支持这样一种语言时。不过,目前该属性仍然是完全可选的。

@import指令

现在我们将讨论位于<style>标签内部的内容。首先,我们有与<link>非常相似的东西,即@import指令:

@import url(sheet2.css);

<link>一样,@import可用于指示 Web 浏览器加载外部样式表,并在 HTML 文档的渲染中使用其样式。唯一的主要区别在于命令的语法和放置位置。正如您所见,@import位于<style>元素内部。它必须放在其他 CSS 规则之前,否则将完全不起作用。考虑以下示例:

<style>
@import url(styles.css); /* @import comes first */
h1 {color: gray;}
</style>

<link>类似,文档可以有多个@import语句。但不同的是,每个@import指令的样式表都会被加载和使用;没有办法指定备用样式表来使用@import。所以,考虑以下标记:

@import url(sheet2.css);
@import url(blueworld.css);
@import url(zany.css);

…所有三个外部样式表都将被加载,并且它们的所有样式规则将在文档的显示中使用。

<link> 类似,您可以通过在样式表的 URL 后提供媒体描述符来限制导入的样式表到一个或多个媒体:

@import url(sheet2.css) all;
@import url(blueworld.css) screen;
@import url(zany.css) screen, print;

如 “The Tag” 中所述,媒体描述符可能会变得非常复杂,并且在 第二十一章 中详细解释。

如果您有一个需要使用其他外部样式表中的样式的外部样式表,则 @import 指令可能会非常有用。由于外部样式表不能包含任何文档标记,因此无法使用 <link> 元素,但可以使用 @import。因此,您可能会有一个包含以下内容的外部样式表:

@import url(http://example.org/library/layout.css);
@import url(basic-text.css);
@import url(printer.css) print;
body {color: red;}
h1 {color: blue;}

嗯,也许不是那些确切的样式,但希望你能理解。请注意前面示例中绝对和相对 URL 的使用。可以使用任一 URL 形式,就像 <link> 一样。

还请注意,@import 指令出现在样式表的开头,就像示例文档中的情况一样。正如我们之前所说,CSS 要求 @import 指令必须在样式表中的任何规则之前出现,尽管它们可以被 @charset@layer 声明所先于。如果 @import 出现在其他规则之后(例如,body {color: red;}),则符合规范的用户代理将忽略它。

警告

某些版本的 Windows Internet Explorer 并不会忽略任何 @import 指令,即使它们出现在其他规则之后,但所有现代浏览器都会忽略放置不正确的 @import 指令。

可以添加到 @import 指令的另一个描述符是 级联层 标识符。这会将导入样式表中的所有样式分配给级联层,这是我们将在 第四章 中探讨的概念。它看起来像这样:

@import url(basic-text.css) screen layer(basic);

basic-text.css 的样式分配给 basic 级联层。如果要将样式分配给未命名的层,则使用 layer 而不是括号内的命名,如下所示:

@import url(basic-text.css) screen layer;

请注意,这是 @import<link> 之间的一个区别,因为后者不能用级联层标记。

HTTP 链接

通过 HTTP 头文件的另一种,远不常见的方法将 CSS 与文档关联起来。

在 Apache HTTP 服务器下,可以通过在 .htaccess 文件中添加对 CSS 文件的引用来实现。例如:

Header add Link "</ui/testing.css>;rel=stylesheet;type=text/css"

这将导致支持的浏览器将引用的样式表与任何从该 .htaccess 文件下提供的文档关联起来。然后,浏览器将其视为链接的样式表。或者,而且可能更有效的是,您可以将等效规则添加到服务器的 httpd.conf 文件中:

<Directory /path/to/ /public/html/directory>
Header add Link "</ui/testing.css>;rel=stylesheet;type=text/css"
</Directory>

在支持的浏览器中效果完全相同。唯一的区别在于声明链接的位置不同。

你可能注意到了术语“支持浏览器”的使用。截至 2022 年底,支持 HTTP 链接样式表的常用浏览器包括 Firefox 系列和 Opera。这限制了该技术主要适用于基于这些浏览器之一的开发环境。在这种情况下,您可以在测试服务器上使用 HTTP 链接来标记您是否在开发站点而不是公共站点上。这也是一个隐藏 Chromium 浏览器样式的有趣方式,假设您有这样的需求。

注意

与此链接技术等效的技术在 PHP 和 IIS 等常见脚本语言中使用,这两者都允许作者发出 HTTP 标头。还可以使用这些语言根据服务器提供文档来显式地编写link元素。从浏览器支持的角度来看,这是一种更为健壮的方法:每个浏览器都支持link元素。

内联样式

如果你只想为单个元素分配一些样式,而不需要嵌入或外部样式表,你可以使用 HTML 属性style

<p style="color: gray;">The most wonderful of all breakfast foods is
the waffle—a ridged and cratered slab of home-cooked, fluffy goodness...
</p>

style属性可以与任何 HTML 标签关联,甚至可以是在<body>之外找到的标签(例如<head><title>)。

style属性的语法非常普通。事实上,它看起来非常像在<style>容器中找到的声明,只是这里用双引号代替了大括号。因此,<p style="color: maroon; background: yellow;">会将段落的文字颜色设置为栗色,背景设置为黄色,仅适用于该段落。文档的其他部分不会受此声明的影响。

请注意,您只能在内联style属性中放置声明块,而不能放置整个样式表。因此,您不能将@import放入style属性中,也不能包含任何完整的规则。您可以将style属性的值放入大括号之间的内容。

不鼓励使用style属性。将样式放入style属性中时,CSS 的许多主要优势——如能够组织集中的样式以控制整个文档的外观或所有文档在 Web 服务器上的外观——都会被抵消。在许多方面,内联样式与古老的<font>标签并没有多大区别,即使它们在应用哪些视觉效果方面具有更大的灵活性。

总结

使用 CSS,您可以通过用户代理完全改变元素呈现的方式。您可以通过display属性在基本层面上进行这样的操作,也可以通过将样式表与文档关联的不同方式进行操作。用户永远不会知道是通过外部样式表、嵌入样式表还是内联样式来完成的。外部样式表的真正重要性在于它们允许您将所有站点的展示信息放在一个地方,并指向所有文档的那个地方。这不仅使得站点的更新和维护变得轻而易举,而且有助于节省带宽,因为所有的展示内容都从文档中移除了。

要充分利用 CSS 的强大功能,您需要知道如何将一组样式与文档中的元素关联起来。要完全理解 CSS 的所有功能,您需要牢牢掌握 CSS 选择文档部分进行样式化的方式,这是接下来几章的主题。

第二章:选择器

CSS 的主要优势之一是能够轻松地将一组样式应用于文档中所有相同类型的元素。不感兴趣?考虑一下:通过编辑一行 CSS,您可以更改所有标题的颜色。不喜欢目前使用的蓝色?更改一行代码,它们可以是紫色、黄色、褐色或您想要的任何其他颜色。

这种能力使您作为作者可以专注于设计和用户体验,而不是繁琐的查找和替换操作。下次开会时,有人想要看到不同颜色的标题,只需编辑您的样式并重新加载。Voilà!几秒钟内就能看到结果,让所有人都能看到。

基本样式规则

如前所述,CSS 的一个核心特性是其能够将某些规则应用于文档中一整组元素类型。例如,假设您想使所有<h2>元素的文本显示为灰色。在有 CSS 之前,您必须在所有<h2>元素中插入<font color="gray">...</font>标签来实现此目的。使用style属性应用内联样式,这也是不良实践,需要您在所有<h2>元素中包含style="color: gray;",如下所示:

<h2 style="color: gray;">This is h2 text</h2>

如果您的文档包含大量<h2>元素,则这将是一个繁琐的过程。更糟糕的是,如果稍后决定要将所有这些<h2>改为绿色而不是灰色,您将不得不重新开始手动标记(是的,这确实是以前的做法!)

CSS 允许您创建简单易变、易编辑和适用于您定义的所有文本元素的规则(下一节将解释这些规则的工作原理)。例如,您可以编写此规则,使所有<h2>元素变为灰色:

h2 {color: gray;}

类型选择器

类型选择器,以前称为元素选择器,最常见的是 HTML 元素,但并非总是如此。例如,如果 CSS 文件包含 XML 文档的样式,则类型选择器可能如下所示:

quote {color: gray;}
bib {color: red;}
booktitle {color: purple;}
myElement {color: red;}

换句话说,文档的元素是被选择的节点类型。在 XML 中,选择器可以是任何东西,因为 XML 允许创建可以具有几乎任何元素名称的新标记语言。如果您正在为 HTML 文档添加样式,则选择器通常将是 HTML 定义的许多元素之一,例如<p><h3><em><a>或甚至<html>本身。例如:

html {color: black;}
h1 {color: gray;}
h2 {color: silver;}

图 2-1 显示了此样式表的结果。

css5 0201

图 2-1。简单文档的简单样式

一旦您直接将样式全局应用于元素,您可以将这些样式从一个元素转移到另一个元素。假设您决定段落文本而不是<h1>元素在图 2-1 中应为灰色。没问题。只需将h1选择器更改为p

html {color: black;}
p {color: gray;}
h2 {color: silver;}

图 2-2 显示了结果。

css5 0202

图 2-2。将样式从一个元素移动到另一个元素

分组

到目前为止,您已经看到了将单个样式应用于单个选择器的相当简单的技术。但是如果您希望相同的样式应用于多个元素怎么办?组合 允许作者大幅压缩某些类型的样式分配,使样式表更短。

组合选择器

假设您想要 <h2> 元素和段落都显示灰色文本。实现这一目标的最简单方法是使用以下声明:

h2, p {color: gray;}

通过将 h2p 选择器放在规则的开头,即在开放的大括号之前,并用逗号分隔它们,您定义了一个规则,表明大括号内的样式(color: gray;)适用于两个选择器引用的元素。逗号告诉浏览器该规则涉及两个不同的选择器。如果省略逗号,规则将具有完全不同的含义,我们将在“定义后代选择器”中探讨这一点。

这些选择产生完全相同的结果,但一种输入起来更容易:

h1 {color: purple;}
h2 {color: purple;}
h3 {color: purple;}
h4 {color: purple;}
h5 {color: purple;}
h6 {color: purple;}

h1, h2, h3, h4, h5, h6 {color: purple;}

第二种选择,使用一个分组选择器,随着时间的推移也更容易维护。

通用选择器

通用选择器,显示为星号(*),匹配任何元素,就像通配符一样。例如,要使文档中的每个元素都加粗,您可以这样写:

* {font-weight: bold;}

此声明相当于一个列出文档中所有元素的分组选择器,使您能够一次性将 font-weightbold 分配给文档中的每个元素。但请注意:虽然通用选择器很方便,因为它针对声明范围内的所有内容,但它可能会产生意想不到的后果,这在“零选择器特异性”中有讨论。

组合声明

正如您可以将选择器组合成单个规则一样,您也可以组合声明。假设您希望所有 <h1> 元素显示为紫色、18 像素高的 Helvetica 文本,并带有水绿色背景(如果不介意让读者眼花缭乱的话),您可以这样写您的样式:

h1 {font: 18px Helvetica;}
h1 {color: purple;}
h1 {background: aqua;}

但是这种方法效率低下——想象一下为将承载 10 或 15 种样式的元素创建这样的列表!相反,您可以将声明分组在一起:

h1 {font: 18px Helvetica; color: purple; background: aqua;}

这将与刚刚显示的三行样式表具有完全相同的效果。

注意,在分组它们时,每个声明末尾使用分号至关重要。浏览器会忽略样式表中的空白,因此用户代理必须依赖正确的语法来解析样式表。您可以毫不犹豫地格式化以下样式:

h1 {
  font: 18px Helvetica;
  color: purple;
  background: aqua;
}

您还可以最小化您的 CSS,删除所有不必要的空格:

h1{font:18px Helvetica;color:purple;background:aqua;}

服务器同样对最后三个示例进行处理,但第二个通常被认为是最易读的,并且是开发过程中推荐的编写 CSS 的方法。你可能会选择为网络性能原因最小化你的 CSS,但这通常会被构建工具、服务器端脚本、缓存网络或其他服务自动处理,因此通常最好以人类可读的方式编写你的 CSS。

如果在第二个语句中省略了分号,用户代理将按以下方式解释样式表:

h1 {
  font: 18px Helvetica;
  color: purple background: aqua;
}

因为background:不是color的有效值,用户代理将完全忽略color声明(包括background: aqua部分)。你可能认为浏览器至少会将<h1>渲染为紫色文本而没有水绿色背景,但事实并非如此。相反,它们将是继承的颜色,具有透明的背景。声明font: 18px Helvetica仍将生效,因为它正确地用分号终止了。

提示

虽然在 CSS 中在规则的最后声明后面跟分号在技术上不是必需的,但这样做通常是个好习惯。首先,它将使你保持习惯,用分号终止你的声明,缺少分号是导致渲染错误最常见的原因之一。其次,如果你决定向规则添加另一个声明,你就不必担心忘记插入额外的分号了。

与选择器分组类似,声明分组是保持你的样式表简短、表达力强和易于维护的便捷方式。

全部分组

现在你知道你可以分组选择器和声明。通过将这两种分组结合在单个规则中,你可以只用几个语句就定义非常复杂的样式。现在,如果你想为文档中的所有标题分配一些复杂的样式,并且希望相同的样式应用于它们所有,那么应该这样做:

h1, h2, h3, h4, h5, h6 {color: gray; background: white; padding: 0.5em;
  border: 1px solid black; font-family: Charcoal, sans-serif;}

我们在这里分组了选择器,所以大括号内的样式将应用于所有列出的标题;分组声明意味着所有列出的样式将应用于规则左侧的选择器。图 2-3 展示了此规则的结果。

css5 0203

图 2-3. 分组选择器和规则

这种方法优于冗长的替代方法,其开头可能是这样的:

h1 {color: gray;}
h2 {color: gray;}
h3 {color: gray;}
h4 {color: gray;}
h5 {color: gray;}
h6 {color: gray;}
h1 {background: white;}
h2 {background: white;}
h3 {background: white;}

……并继续多行。你可以用长篇方式编写你的样式,但我们不推荐这样做——编辑它们将会像在每个地方使用style属性一样繁琐!

分组允许一些有趣的选择。例如,以下示例中所有规则组都是等效的——每个仅展示了一种不同的分组选择器和声明的方式:

/* group 1 */
h1 {color: silver; background: white;}
h2 {color: silver; background: gray;}
h3 {color: white; background: gray;}
h4 {color: silver; background: white;}
b {color: gray; background: white;}

/* group 2 */
h1, h2, h4 {color: silver;}
h2, h3 {background: gray;}
h1, h4, b {background: white;}
h3 {color: white;}
b {color: gray;}

/* group 3 */
h1, h4 {color: silver; background: white;}
h2 {color: silver;}
h3 {color: white;}
h2, h3 {background: gray;}
b {color: gray; background: white;}

任何这三种选择器和声明分组方法都将产生图 2-4 中显示的结果。

css5 0204

图 2-4. 等效样式表的结果

类和 ID 选择器

到目前为止,我们已经以各种方式将选择器和声明分组在一起,但是我们使用的选择器非常简单,只引用文档元素。类型选择器在某种程度上是可以的,但通常情况下,您需要更加专注的内容。

除了类型选择器之外,CSS 还有类选择器ID 选择器,这些选择器允许您根据 HTML 属性而不是元素类型分配样式。这些选择器可以单独使用或与类型选择器结合使用。然而,它们仅在您适当地标记文档时才有效,因此通常需要一些事先考虑和规划。

例如,假设文档包含多个警告。您希望每个警告都以粗体文本显示,以便突出显示。但是,您不知道哪些元素类型包含此警告内容。有些警告可能是整个段落,而其他可能是长列表中的单个项目或文本部分中的几个单词。因此,您无法使用任何类型选择器来定义规则。假设您尝试了这条路线:

p {
  font-weight: bold;
  color: red;
}

所有段落都将是红色和粗体,而不仅仅是那些包含警告的段落。您需要一种方法来仅选择包含警告文本的文本——更准确地说,是一种仅选择那些警告元素的方法。您该如何做到呢?通过使用类选择器,您可以对文档的某些部分应用样式,而与所涉及的元素无关。

类选择器

应用样式的最常见方法是使用类选择器,而无需担心所涉及的元素。但是,在使用它们之前,您需要修改文档标记,以使类选择器正常工作。输入class属性:

<p class="warning">When handling plutonium, care must be taken to avoid
the formation of a critical mass.</p>
<p>With plutonium, <span class="warning">the possibility of implosion is
very real, and must be avoided at all costs</span>. This can be accomplished
by keeping the various masses separate.</p>

要将类选择器的样式与元素关联起来,必须为class属性分配适当的值。在前面的代码块中,将classwarning分配给两个元素:第一个段落和第二段落中的<span>元素。

要对这些带类的元素应用样式,可以使用一种紧凑的表示法,其中类名前面带有一个句点(.):

*.warning {font-weight: bold;}

当与前面显示的示例标记结合使用时,此简单规则的效果如图 2-5 所示。声明font-weight: bold将应用于携带class属性值为warning的每个元素。

如图 2-5 所示,类选择器通过直接引用元素的class属性中的值来工作。这个引用总是以一个句点(.)开头,标志着它是一个类选择器。这个句点有助于将类选择器与可能与之组合的任何内容分开,比如类型选择器。例如,你可能只想在整个段落作为警告时使用加粗的警告文本:

p.warning {font-weight: bold;}

css5 0205

图 2-5. 使用类选择器

此选择器现在匹配任何具有包含单词warningclass属性的 <p> 元素,但不包括其他任何类型的元素,无论其类别如何。由于 <span> 元素不是段落,规则的选择器不匹配它,因此不会使用加粗文本显示它。

如果你想为<span>元素分配不同的样式,可以使用选择器 span.warning

p.warning {font-weight: bold;}
span.warning {font-style: italic;}

在这种情况下,警告段落加粗,而警告的 <span> 斜体。每个规则只适用于特定类型的元素/类组合,因此不会泄漏到其他元素上。

另一种选择是使用通用类选择器和元素特定类选择器的组合,使样式更加实用,如以下标记所示:

.warning {font-style: italic;}
span.warning {font-weight: bold;}

图 2-6 展示了结果。

在这种情况下,任何警告文本都将是斜体,但只有具有classwarning<span> 元素中的文本既加粗又斜体。

css5 0206

图 2-6. 使用通用和特定选择器组合样式
小贴士

注意前面示例中使用的通用类选择器的格式:它是一个类名,前面加一个句点,没有元素名或通配选择器。如果你想选择所有具有相同类名的元素,你可以省略类选择器中的通配选择器而不会产生任何不良影响。因此,*.warning.warning 的效果完全相同。

关于类名的另一点是:它们永远不应该以数字开头。浏览器可能会允许你这样做,但是 CSS 验证器会抱怨,而且这是一个不好的习惯。因此,你应该在 CSS 中写 .c8675,在 HTML 中写 class="c8675",而不是写 .8675class="8675"。如果你必须引用以数字开头的类名,应该在类选择器的句点和第一个数字之间加一个反斜杠,像这样:.\8675

多个类

在上一节中,我们处理了包含单个单词的class值。在 HTML 中,可以在一个class值中使用空格分隔的单词列表。例如,如果要将特定元素标记为既紧急又警告,可以这样写:

<p class="urgent warning">When handling plutonium, care must be taken to
avoid the formation of a critical mass.</p>
<p>With plutonium, <span class="warning">the possibility of implosion is
very real, and must be avoided at all costs</span>. This can be accomplished
by keeping the various masses separate.</p>

单词的顺序并不重要;warning urgent同样有效,并且无论 CSS 如何编写,都将产生完全相同的结果。与 HTML 标签和类型选择器不同,类选择器是区分大小写的。

现在假设你希望所有classwarning的元素为粗体,classurgent的元素为斜体,并且同时具有这两个值的元素具有银色背景。写成如下:

.warning {font-weight: bold;}
.urgent {font-style: italic;}
.warning.urgent {background: silver;}

通过链接两个类选择器,你可以选择只有这两个类名的元素,无论顺序如何。正如你所看到的,HTML 源代码包含class="urgent warning",但 CSS 选择器写成.warning.urgent。无论如何,该规则仍将导致“处理钚时……”段落具有银色背景,如图 2-7 所示。这是因为源文档或 CSS 中单词的排列顺序并不重要。(这并不是说类的顺序总是无关紧要,但我们稍后会在本章中讨论。)

css5 0207

图 2-7。选择具有多个类名的元素

如果多类选择器包含一个不在空格分隔列表中的名称,则匹配将失败。考虑以下规则:

p.warning.help {background: red;}

正如你所预料的那样,选择器只会匹配那些<p>元素,其class属性包含空格分隔的单词warninghelp。因此,它不会匹配class属性中仅包含单词warningurgent<p>元素。然而,它会匹配以下内容:

<p class="urgent warning help">Help me!</p>

ID 选择器

在某些方面,ID 选择器与类选择器类似,但存在一些关键的区别。首先,ID 选择器以井号(#)开头——在正式名称上称为井号,在美国也称为磅号或井字号。因此,你可能会看到像这样的规则:

*#first-para {font-weight: bold;}

此规则会使具有值为first-paraid属性的任何元素的文本变为粗体。

第二个区别在于,ID 选择器不是引用class属性的值,而是合理地引用id属性中的值。以下是 ID 选择器的示例:

*#lead-para {font-weight: bold;}
<p id="lead-para">This paragraph will be boldfaced.</p>
<p>This paragraph will NOT be bold.</p>

注意,lead-para的值可以分配给文档中的任何元素。在这种特定情况下,它应用于第一个段落,但我们同样可以将其轻松应用于第二个或第三个段落,或无序列表中的任何元素。

第三个区别在于文档中应该只有一个给定 ID 值的实例。如果你发现自己想要将同一个 ID 应用于文档中的多个元素,请改用类(class)。

与类选择器一样,可以(并且通常)从 ID 选择器中省略通配符选择器。在上一个示例中,我们也可以这样写,并具有完全相同的效果:

#lead-para {font-weight: bold;}

当您知道文档中将会出现某个特定的 ID 值,但不知道它将出现在哪种元素类型上时,这是非常有用的。例如,您可能知道在任何给定的文档中,将存在一个具有 ID 值为mostImportant的元素。您不知道这个最重要的事物是段落、短语、列表项还是章节标题。您只知道它将在每个文档中存在,在任意元素中出现,并且最多出现一次。在这种情况下,您可以编写如下规则:

#mostImportant {color: red; background: yellow;}

这条规则将匹配以下任何一个元素(注意,这些元素不应该同时出现在同一个文档中,因为它们都具有相同的 ID 值):

<h1 id="mostImportant">This is important!</h1>
<em id="mostImportant">This is important!</em>
<ul id="mostImportant">This is important!</ul>

虽然 HTML 标准规定文档中每个id必须唯一,但 CSS 并不关心。如果我们误将刚才显示的 HTML 包含进来,所有三个元素都很可能会因为匹配#mostImportant选择器而变成红色带黄色背景。

注意

与 class 名称一样,ID 名称不应以数字开头。如果必须引用以数字开头且无法更改标记中的 ID 值的 ID,请在第一个数字前使用反斜杠,如#\309

在选择 Class 和 ID 之间做出决定

您可以将类分配给任意数量的元素,正如前面演示的那样;warning类名应用于<p><span>元素,并且还可以应用于更多元素。另一方面,ID 值应该在 HTML 文档中仅使用一次。因此,如果您有一个带有 ID 值为lead-para的元素,则该文档中不应有其他具有 ID 值为lead-para的元素。

不过,这只是根据 HTML 规范来说的。正如前面提到的,CSS 不在乎你的 HTML 是否有效:它应该找到匹配选择器的所有元素。这意味着,如果在 HTML 文档中散布了几个具有相同 ID 属性值的元素,那么应该将相同的样式应用于每个元素。

注意

在文档中有多个相同的 ID 值会使 DOM 脚本编写变得更加困难,因为像getElementById()这样的函数依赖于具有给定 ID 值的一个且仅有一个元素。

与类选择器不同,ID 选择器不能与其他 ID 结合使用,因为 ID 属性不允许空格分隔的单词列表。但是,ID 选择器可以与自身结合:#warning#warning将匹配具有 ID 值为warning的元素。虽然这几乎永远不应该这样做,但确实是可能的。

另一个classid名称之间的区别在于,在确定应将哪些样式应用于给定元素时,ID 权重更大。这在第四章中有更详细的解释。

还要注意,HTML 定义类和 ID 值为大小写敏感,因此您的类和 ID 值的大写必须与文档中找到的匹配。因此,在以下 CSS 和 HTML 配对中,元素的文本将不会加粗:

p.criticalInfo {font-weight: bold;}
<p class="criticalinfo">Don't look down.</p>

因为字母i的大小写变化,选择器将不匹配所示的元素。

纯语法上来说,点类表示法(例如.warning)不保证在 XML 文档中有效。截至本文撰写时,点类表示法适用于 HTML、可缩放矢量图形(SVG)和数学标记语言(MathML),并且可能在未来的语言规范中被允许,但这取决于每种语言的规范。

属性选择器

对于类和 ID 选择器,您实际上是选择元素属性的值。在本文撰写时使用的语法特定于 HTML、SVG 和 MathML 文档。在其他标记语言中,这些类和 ID 选择器可能不可用(事实上,这些属性可能不存在)。

为了解决这种情况,CSS2 引入了属性选择器,可以根据其属性和属性值来选择元素。有四种常见类型的属性选择器:简单属性选择器、精确属性值选择器、部分匹配属性值选择器和前导值属性选择器。

简单属性选择器

如果您想选择具有某个属性的元素,而不管该属性的值如何,您可以使用简单属性选择器。例如,要选择所有具有任何值的class属性的<h1>元素,并使它们的文本变为银色,可以写成这样:

h1[class] {color: silver;}

因此,鉴于以下标记,

<h1 class="hoopla">Hello</h1>
<h1>Serenity</h1>
<h1 class="fancy">Fooling</h1>

结果如图 2-8 所示。

css5 0208

图 2-8. 根据其属性选择元素

这种策略在 XML 文档中非常有用,因为 XML 语言往往具有特定于其目的的元素和属性名称。考虑一种用于描述太阳系行星的 XML 语言(我们称之为PlanetML)。如果要选择所有具有moons属性的<pml-planet>元素并使它们加粗,从而突出显示具有卫星的任何行星,您应该这样写:

pml-planet[moons] {font-weight: bold;}

这将导致以下标记片段中的第二个和第三个元素的文本加粗,但第一个元素不加粗:

<pml-planet>Venus</pml-planet>
<pml-planet moons="1">Earth</pml-planet>
<pml-planet moons="2">Mars</pml-planet>

ID 哈希表示法(例如#lead)应在具有属性值在文档中应该是唯一的任何文档语言中有效。在 HTML 文档中,您可以以创造性的方式使用此功能。例如,您可以为所有具有alt属性的图像设置样式,从而突出显示那些正确形成的图像。

img[alt] {outline: 3px solid forestgreen;}

这个特定的示例通常更适合用于诊断目的——确定图像是否确实正确标记——而不是设计目的。

如果你想要加粗包含title信息的任何元素,大多数浏览器将其显示为鼠标悬停在元素上时的工具提示,你可以这样写:

*[title] {font-weight: bold;}

类似地,您可以仅对具有href属性的锚点(<a>元素)进行样式设置,从而将样式应用于任何超链接,但不适用于任何占位符锚点。

也可以基于多个属性的存在来选择元素。通过链接属性选择器来实现。例如,要加粗任何具有hreftitle属性的 HTML 超链接的文本,您将写成以下内容:

a[href][title] {font-weight: bold;}

这将加粗以下标记中的第一个链接,但不会影响第二个或第三个:

<a href="https://www.w3.org/" title="W3C Home">W3C</a><br />
<a href="https://developer.mozilla.org">Standards Info</a><br />
<a title="Not a link">dead.letter</a>

根据精确属性值的选择

您可以进一步缩小选择范围,以仅包含其属性值为特定值的元素。例如,假设您想要加粗指向 Web 服务器上特定文档的任何超链接。这将看起来像以下内容:

a[href="http://www.css-discuss.org/about.html"] {font-weight: bold;}

这将加粗任何a元素的文本,该元素具有href属性,其值恰好http://www.css-discuss.org/about.html。任何变动,即使是省略www.部分或更改为安全协议https,都将阻止匹配。

任何元素都可以指定任何属性和值组合。但是,如果该确切组合在文档中不存在,选择器将不匹配任何内容。同样,XML 语言可以从这种样式方法中受益。让我们回到我们的 PlanetML 示例。假设您只想选择那些具有属性moons1值的planet元素:

planet[moons="1"] {font-weight: bold;}

这将加粗以下标记片段中的第二个元素的文本,但不会影响第一个或第三个:

<planet>Venus</planet>
<planet moons="1">Earth</planet>
<planet moons="2">Mars</planet>

与属性选择类似,您可以将多个属性值选择器链接在一起以选择单个文档。例如,要加倍于https://www.w3.org/具有href值和title值为W3C Home的任何 HTML 超链接的文本大小,您将写成这样:

a[href="https://www.w3.org/"][title="W3C Home"] {font-size: 200%;}

这将加倍以下标记中的第一个链接的文本大小,但不会影响第二个或第三个:

<a href="https://www.w3.org/" title="W3C Home">W3C</a><br />
<a href="https://developer.mozilla.org"
  title="Mozilla Developer Network">Standards Info</a><br />
<a href="http://www.example.org/" title="W3C Home">confused.link</a>

图 2-9 显示了结果。

css5 0209

图 2-9. 根据属性及其值选择元素

再次强调,此格式需要属性值的精确匹配。当属性选择器遇到可以包含空格分隔值列表的值时(例如 HTML 属性class),匹配就会成为一个问题。例如,考虑以下标记片段:

<planet type="barren rocky">Mercury</planet>

唯一匹配该元素基于其精确属性值的方法是写成这样:

planet[type="barren rocky"] {font-weight: bold;}

如果您写成planet[type="barren"],该规则将不会匹配示例标记,因此会失败。即使对于 HTML 中的class属性也是如此。考虑以下示例:

<p class="urgent warning">When handling plutonium, care must be taken to
avoid the formation of a critical mass.</p>

要基于其精确的属性值选择此元素,您必须编写如下内容:

p[class="urgent warning"] {font-weight: bold;}

这与之前介绍的点类记法不等同,如下一节所示。它实际上选择任何 class 属性值 完全urgent warningp 元素,其中单词顺序不变,单词之间有一个空格。这实际上是一个精确的字符串匹配,而在使用 class 选择器时,类的顺序并不重要。

另外,请注意 ID 选择器和目标 id 属性的属性选择器并不完全相同。换句话说,h1#page-titleh1[id="page-title"] 之间存在微妙但关键的差别。这一差异在 第四章 中有解释。

基于部分属性值的选择

有时你可能希望基于其属性值的部分匹配选择元素,而不是完整值。对于这种情况,CSS 提供了多种选项用于匹配属性值中的子串。这些总结在 表 2-1 中。

表 2-1. 属性选择器的子串匹配

类型 描述
[foo~="bar"] 选择具有属性 foo 并且其值在空格分隔的单词列表中包含单词 bar 的任何元素
[foo*="bar"] 选择具有属性 foo 并且其值包含子串 bar 的任何元素
[foo^="bar"] 选择具有属性 foo 并且其值 bar 开头的任何元素
[foo$="bar"] 选择具有属性 foo 并且其值 bar 结尾的任何元素
[foo&#124;="bar"] 选择具有属性 foo 并且其值 bar 开头,并紧跟一个连字符(U+002D),或其值正好等于 bar 的任何元素

最后一个匹配元素属性值部分子集的属性选择器比描述起来更容易显示。考虑以下规则:

*[lang|="en"] {color: white;}

此规则将选择任何 lang 属性等于 en 或以 en- 开头的元素。因此,下面示例标记中的前三个元素将被选择,而后两个则不会:

<h1 lang="en">Hello!</h1>
<p lang="en-us">Greetings!</p>
<div lang="en-au">G'day!</div>
<p lang="fr">Bonjour!</p>
<h4 lang="cy-en">Jrooana!</h4>

通常情况下,形式 [att|="val"] 可以用于任何属性及其值。假设在 HTML 文档中有一系列文件名类似于 figure-1.giffigure-3.jpg 的图像。你可以通过以下选择器匹配所有这些图像:

img[src|="figure"] {border: 1px solid gray;}

或者,如果你正在创建一个 CSS 框架或模式库,而不是创建冗余的类如 "btn btn-small btn-arrow btn-active",你可以声明 "btn-small-arrow-active",并且通过以下方式定位元素的类:

*[class|="btn"] { border-radius: 5px;}

<button class="btn-small-arrow-active">Click Me</button>

这种类型的属性选择器最常见的用途是匹配语言值,如 “The :lang() and :dir() Pseudo-Classes” 中演示的那样。

匹配空格分隔列表中的一个单词

对于接受空格分隔单词列表的任何属性,你可以根据任何一个单词的存在选择元素。HTML 中的经典示例是class属性,它可以接受一个或多个单词作为其值。考虑我们通常的示例文本:

<p class="urgent warning">When handling plutonium, care must be taken to
avoid the formation of a critical mass.</p>

假设你想选择class属性包含单词warning的元素。你可以使用属性选择器来实现这一点:

p[class~="warning"] {font-weight: bold;}

注意选择器中的波浪号(~)的存在。这是基于属性值中存在空格分隔单词进行选择的关键。如果省略波浪号,你将得到一个精确值匹配的属性选择器,正如前一节中讨论的那样。

这种选择器构造等效于“在类和 ID 之间做出决定”中讨论的点类记法。因此,p.warningp[class~="warning"]在应用于 HTML 文档时是等效的。以下是早期看到的 PlanetML 标记的 HTML 版本示例:

<span class="barren rocky">Mercury</span>
<span class="cloudy barren">Venus</span>
<span class="life-bearing cloudy">Earth</span>

要使所有class属性中含有单词barren的元素变为斜体,你可以这样写:

span[class~="barren"] {font-style: italic;}

此规则的选择器将匹配示例标记中的前两个元素,从而使它们的文本变为斜体,如 Figure 2-10 所示。这与编写span.barren {font-style: italic;}得到的结果相同。

css5 0210

图 2-10. 基于属性值部分选择元素

那么为什么在 HTML 中要使用波浪号等号属性选择器呢?因为它可以用于任何属性,而不仅仅是class。例如,你可能有一个包含大量图像的文档,其中只有一些是图表。你可以使用针对title文本的部分匹配值属性选择器,仅选择那些图表:

img[title~="Figure"] {border: 1px solid gray;}

此规则选择任何标题文本包含单词Figure(但不包含figure,因为标题属性区分大小写)的图像。因此,只要你的所有图像的标题文本看起来像“Figure 4. 一位秃头长者”,这条规则就会匹配这些图像。此外,选择器img[title~="Figure"]也会匹配值为“How to Figure Out Who’s in Charge.”的标题属性。任何没有title属性或其title值不包含单词Figure的图像都不会被匹配。

在属性值中匹配子字符串

有时候你想基于其属性值的一部分选择元素,但是相关的值并不是空格分隔的单词列表。在这些情况下,你可以使用星号等号子字符串匹配形式[attr*="val"]来匹配出现在属性值的任何地方的子字符串。例如,以下 CSS 匹配任何<span>元素,其class属性包含子字符串cloud,因此两个“cloudy”行星都会被匹配,如 Figure 2-11 所示:

span[class*="cloud"] {font-style: italic;}
<span class="barren rocky">Mercury</span>
<span class="cloudy barren">Venus</span>
<span class="life-bearing cloudy">Earth</span>

css5 0211

图表 2-11。选择基于属性值中子字符串的元素

注意选择器中星号(*)的存在。它是基于属性值中存在子字符串的元素选择的关键。需要明确的是,它与通用选择器没有关系,只是使用了相同的字符。

您可以想象,这种特定功能有许多有用的应用。例如,假设您希望特别样式化任何链接到 W3C 网站的链接。您可以不用给它们全部添加类并基于该类编写样式,而是可以编写以下规则:

a[href*="w3.org"] {font-weight: bold;}

您不仅仅限于classhref属性。这里可以使用任何属性(titlealtsrcid…);如果属性具有值,您可以基于该值中的子字符串进行样式设置。以下规则用于突出显示任何源 URL 中包含字符串space的图像:

img[src*="space"] {outline: 5px solid red;}

同样地,以下规则用于突出显示具有指导用户操作的标题的<input>元素,以及其标题中包含子字符串format的任何其他输入:

input[title*="format"] {background-color: #dedede;}
<input type="tel"
    title="Telephone number should be formatted as XXX-XXX-XXXX"
    pattern="\d{3}\-\d{3}\-\d{4}">

通用子字符串属性选择器的常见用法是匹配模式库类名中的部分。在前面的例子的基础上,我们可以通过使用管道等于属性选择器来针对任何以btn开头并且包含以连字符前缀的arrow子字符串的类名进行目标定位:

*[class|="btn"][class*="-arrow"]:after { content: "▼";}
<button class="btn-small-arrow-active">Click Me</button>

匹配是精确的:如果您在选择器中包含空格,则属性值中也必须存在空格。当底层文档语言要求大小写敏感性时,属性值也是大小写敏感的。类名、标题、URL 和 ID 值都是大小写敏感的,但枚举的 HTML 属性值,如输入类型关键字值,则不是:

input[type="CHeckBoX"] {margin-right: 10px;}
<input type="checkbox" name="rightmargin" value="10px">

匹配以属性值开始的子字符串

如果您希望基于属性值开头的子字符串选择元素,则您需要查找caret-equals属性选择器模式[att^="val"]。当您想要以不同样式显示不同类型的链接时,这种方法特别有用,如在图表 2-12 中所示:

a[href^="https:"] {font-weight: bold;}
a[href^="mailto:"] {font-style: italic;}

css5 0212

图表 2-12。选择基于以属性值开头的子字符串的元素

在另一个用例中,您可能希望为文章中所有同时也是图表的图片设置样式,就像您在本文中看到的图表一样。假设每个图表的alt文本都以“Figure 5”模式的文本开头——在这种情况下这是完全合理的假设——您可以使用带有caret-equals属性选择器来仅选择这些图片:

img[alt^="Figure"] {border: 2px solid gray;  display: block; margin: 2em auto;}

这里的潜在缺点是,任何<img>元素,其altFigure开头,都将被选中,无论它是否意图作为说明性图表。这种可能性取决于所讨论的文档。

另一个用例是选择所有发生在星期一的日历事件。在这种情况下,假设所有事件都有一个包含“星期一,2012 年 3 月 5 日”格式日期的title属性。只需使用[title^="Monday"]就可以选择它们全部。

匹配属性值末尾的子串

与开始子串匹配的镜像是结束子串匹配,这通过[att$="val"]模式实现。这种功能的一个非常常见的用途是根据资源类型为其目标的链接设置不同的样式,例如 PDF 文档,如图 2-13 所示:

a[href$=".pdf"] {font-weight: bold;}

css5 0213

图 2-13. 根据结束属性值子串选择元素

类似地,您可以(出于任何原因)使用美元等于属性选择器选择基于其图像格式的图像:

img[src$=".gif"] {...}
img[src$=".jpg"] {...}
img[src$=".png"] {...}

继续上一节中的日历示例,可以使用像[title$="2015"]这样的选择器选择给定年份内的所有事件。

注意

您可能已经注意到,我们在属性选择器中引用了所有属性值。如果值包含任何特殊字符,以连字符或数字开头,或者以其他方式无效作为标识符并且需要引用为字符串,则需要引用。为了安全起见,我们建议始终在属性选择器中引用属性值,即使只有将无效标识符转换为字符串才需要。

不区分大小写标识符

在属性选择器的结束括号之前加上i将允许该选择器以不区分大小写的方式匹配属性值,而不考虑文档语言规则。例如,假设您想选择所有指向 PDF 文档的链接,但不知道它们是否以.pdf.PDF.Pdf结尾。以下是如何操作:

a[href$='.PDF' i]

添加那个简单的i意味着选择器将匹配所有href属性值以.pdf结尾的a元素,无论字母PDF的大小写如何。

我们已经涵盖的所有属性选择器都提供了此不区分大小写选项。但请注意,这仅适用于属性选择器中的。它不会强制在属性名称本身上不区分大小写。因此,在大小写敏感的语言中,planet[type*="rock" i]将匹配以下所有内容:

<planet type="barren rocky">Mercury</planet>
<planet type="cloudy ROCKY">Venus</planet>
<planet type="life-bearing Rock">Earth</planet>

它将匹配以下元素,因为 XML 中的属性TYPEtype不匹配:

<planet TYPE="dusty rock">Mars</planet>

这是在强制元素和属性语法区分大小写的语言中。在像 HTML 这样的大小写不敏感的语言中,这不是问题。

注意

提议的镜像标识符s,强制区分大小写。截至 2023 年初,仅 Firefox 系列浏览器支持该标识符。

使用文档结构

CSS 如此强大,因为它利用文档的结构来确定适当的样式及其应用方式。在继续探讨更强大的选择形式之前,让我们花一点时间讨论结构。

理解父子关系

要理解选择器与文档之间的关系,我们需要再次查看文档的结构。考虑这个非常简单的 HTML 文档:

<!DOCTYPE html>
<html lang="en-us">
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width">
 <title>Meerkat Central</title>
</head>
<body>
 <h1>Meerkat <em>Central</em></h1>
 <p>
 Welcome to Meerkat <em>Central</em>, the <strong>best meerkat web site
 on <a href="inet.html">the <em>entire</em> Internet</a></strong>!</p>
 <ul>
  <li>We offer:
   <ul>
    <li><strong>Detailed information</strong> on how to adopt a meerkat</li>
    <li>Tips for living with a meerkat</li>
    <li><em>Fun</em> things to do with a meerkat, including:
     <ol>
      <li>Playing fetch</li>
      <li>Digging for food</li>
      <li>Hide and seek</li>
     </ol>
    </li>
   </ul>
  </li>
  <li>...and so much more!</li>
 </ul>
 <p>
 Questions? <a href="mailto:suricate@meerkat.web">Contact us!</a>
 </p>
</body>
</html>

CSS 的很大一部分功能依赖于元素之间的父子关系。HTML 文档(以及大多数结构化文档)基于元素的层次结构,这在文档的“树状”视图中可见(见图 2-14)。在这个层次结构中,每个元素都在文档的整体结构中占据一个位置。文档中的每个元素都是另一个元素的父元素子元素,有时两者兼而有之。如果一个父元素有多个子元素,则这些子元素被称为兄弟元素

css5 0214

图 2-14. 文档树结构

如果一个元素在文档层级结构中直接出现在另一个元素的上方,则称它是另一个元素的父元素。例如,在图 2-14 中,从左边数第一个<p>元素是<em><strong>元素的父元素,而<strong>是锚点(<a>)元素的父元素,该锚点元素本身又是另一个<em>元素的父元素。相反地,如果一个元素直接位于另一个元素的下方,则称它是另一个元素的子元素。因此,图 2-14 最右侧的锚点元素是<p>元素的子元素,而该<p>元素又是<body>元素的子元素,依此类推。

父元素子元素祖先后代这两个术语的具体应用。它们之间有所区别:在树状视图中,如果一个元素恰好在另一个元素的一级上方或下方,则这些元素具有父子关系。如果从一个元素到另一个元素的路径通过两个或更多级别进行跟踪,则这些元素具有祖先后代关系,但不具有父子关系。(子元素也是后代,父元素也是祖先。)在图 2-14 中,最上层的<ul>元素是两个<li>元素的父元素,但最上层的<ul>元素也是从其<li>元素下溯到最深嵌套的<li>元素的每个元素的祖先。这些<li>元素是<ol>的子元素。

在图 2-14 中,有一个锚点,它是<strong>的子元素,同时也是<p><body><html>元素的后代。<body>元素是浏览器默认显示的所有内容的祖先,而<html>元素则是整个文档的祖先。因此,在 HTML 文档中,<html>元素也被称为根元素

定义后代选择器

理解这一模型的第一个好处是能够定义后代选择器。定义后代选择器是创建在特定结构情况下但不在其他情况下生效的规则。举个例子,假设你想要样式化只有从<h1>元素继承而来的<em>元素。要实现这一点,写下以下内容:

h1 em {color: gray;}

这条规则将使得属于<h1>元素后代的<em>元素中的文本变成灰色。其他地方的<em>文本,比如段落或者块引用中的,不会被此规则选中。图 2-15 展示了结果。

css5 0215

图 2-15. 根据上下文选择元素

在后代选择器中,规则的选择器部分由两个或多个用空格分隔的选择器组成。选择器之间的空格是组合器的一个例子。每个空格组合器可以被翻译为“在...中找到”,“是...的一部分”,或者“是...的后代”,但只有当你从右到左读选择器时才有效。因此,h1 em可以翻译为“任何<h1>元素的后代<em>元素”。

要从左到右读选择器,你可能会用更冗长和令人困惑的方式表达,比如,“任何<h1>包含一个<em>将对<em>应用以下样式”。这样说起来更啰嗦和混乱,这也是为什么我们和浏览器一样,从右到左读取选择器。

你并不局限于两个选择器。例如:

ul ol ul em {color: gray;}

在这种情况下,正如图 2-16 所示,任何作为无序列表一部分的有序列表一部分的无序列表一部分的强调文本(是的,这是正确的)将会是灰色的。这显然是一个非常具体的选择标准。

css5 0216

图 2-16. 一个非常具体的后代选择器

后代选择器可以非常强大。让我们考虑一个常见的例子。假设你有一个包含侧边栏和主要区域的文档。侧边栏有蓝色背景,主要区域有白色背景,并且两个区域都包括链接列表。你不能将所有链接都设置为蓝色,因为在侧边栏中它们将无法阅读,你也不能将所有链接都设置为白色,因为它们在页面的主体部分将消失。

解决方案:后代选择器。在这种情况下,给包含侧边栏的元素一个sidebar类,并将页面的主体部分包裹在<main>元素中。然后,写下这样的样式:

.sidebar {background: blue;}
main {background: white;}
.sidebar a:any-link {color: white;}
main a:any-link {color: blue;}

图 2-17 展示了结果。

css5 0217

图 2-17. 使用后代选择器对相同类型的元素应用不同样式
注意

:any-link指的是已访问和未访问的链接。我们将在第三章详细讨论它。

再举一个例子:假设你希望将任何属于blockquote<b>(加粗)元素的文本颜色设为灰色,并为普通段落中找到的任何加粗文本设置为灰色:

blockquote b, p b {color: gray;}

结果是段落或块引用中的<b>元素内的文本将变成灰色。

后代选择器被忽视的一个方面是两个元素之间的分离程度可以是实际上无限的。例如,如果你写ul em,那么这个语法将选择任何从<ul>元素后代到的<em>元素,无论<em>嵌套多深。因此,ul em将选择以下标记中的<em>元素:

<ul>
  <li>List item 1
    <ol>
      <li>List item 1-1</li>
      <li>List item 1-2</li>
      <li>List item 1-3
        <ol>
          <li>List item 1-3-1</li>
          <li>List item <em>1-3-2</em></li>
          <li>List item 1-3-3</li>
        </ol>
      </li>
      <li>List item 1-4</li>
    </ol>
  </li>
</ul>

后代选择器更微妙的一个方面是它们没有元素接近性的概念。换句话说,在文档树中两个元素的接近程度不影响规则的应用。在特异性(我们将在下一章讨论)和考虑可能互相抵消的规则时,这一点非常重要。

例如,考虑以下内容(其中包含:not(),我们将在“否定伪类”中讨论):

div:not(.help) span {color: gray;}
div.help span {color: red;}
<div class="help">
   <div class="aside">
      This text contains <span>a span element</span> within.
   </div>
</div>

CSS 的效果是:“任何位于不具有包含词helpclass<div>内的<span>应该是灰色”,在第一条规则中,以及“任何位于具有包含词helpclass<div>内的<span>”在第二条规则中。在给定的标记片段中,两条规则都适用于显示的<span>

因为两条规则具有相等的特异性权重,并且red规则写在最后,因此它胜出,<span>变成了红色。div class="aside"div class="help"“更接近”<span>这一事实无关紧要。再次强调:后代选择器没有元素接近性的概念。两条规则都匹配,只能应用一种颜色,因为 CSS 的工作方式,这里红色是赢家。(我们将在下一章讨论为什么如此。)

注意

自 2023 年初以来,已经提出了通过选择器作用域向 CSS 添加元素接近性意识的提案,但这些提案仍在积极修订中,可能不会实现。

选择子元素

在某些情况下,你不希望选择任意后代元素。相反,你希望缩小范围,只选择特定作为另一个元素的子元素的元素。例如,你可能只想选择<h1>元素的子元素<strong>元素(而不是任何其他层级的后代)。为此,你可以使用子选择器,即大于号(>):

h1 > strong {color: red;}

这条规则将使第一个<h1>中显示的<strong>元素变成红色,但不会影响第二个:

<h1>This is <strong>very</strong> important.</h1>
<h1>This is <em>really <strong>very</strong></em> important.</h1>

从右向左阅读,选择器 h1 > strong 的翻译为:“选择任何 <strong> 元素,该元素是 <h1> 元素的直接子元素。” 子选择器可以选择是否在周围加上空白。因此,h1 > strongh1> strongh1>strong 都是等效的。你可以根据需要使用或省略空白。

当将文档视为树结构时,我们可以看到子选择器将其匹配限制为树中直接连接的元素。图 2-18 展示了文档树的一部分。

css5 0218

图 2-18. 一个文档树片段

在这个树片段中,你可以确定父子关系。例如,<a> 元素是 <p> 元素的父元素,同时也是 <strong> 的子元素。你可以用选择器 p > aa > strong 来匹配这个片段,但不能用 p > strong,因为 <strong><p> 的后代而不是其子元素。

你也可以在同一选择器中结合后代选择器和子选择器。因此,table.summary td > p 将选择任何 <p> 元素,该元素是一个 元素,它的 <td> 元素本身是 后代 元素,该后代元素是一个包含单词 summaryclass 属性的 <table> 元素。

选择相邻兄弟元素

假设你想为紧跟在标题后的段落设置样式,或者为紧跟在段落后的列表设置特殊边距。要选择紧跟在另一个具有相同父级的元素后的元素,你可以使用 相邻兄弟选择器,表示为加号(+)。与子选择器一样,你可以自由选择是否在符号周围加上空白。

要去除紧跟在 <h1> 元素后的段落的顶部边距,请写入这段代码:

h1 + p {margin-top: 0;}

选择器的解读是:“选择任何紧随一个与 <p> 元素共享父级的 <h1> 元素后的 <p> 元素。”

为了直观地了解这个选择器的工作原理,让我们再次考虑文档树的片段,如 图 2-19 所示。

css5 0219

图 2-19. 另一个文档树片段

在这个片段中,一对列表来自于一个 <div> 元素,一个有序,另一个无序,每个列表都包含三个列表项。每个列表是相邻的兄弟元素,列表项本身也是相邻的兄弟元素。然而,第一个列表的列表项 是第二个列表的兄弟元素,因为这两组列表项没有共享相同的父元素。(充其量,它们是表兄弟,而 CSS 没有表兄弟选择器。)

请记住,你只能使用单个选择器选择两个相邻兄弟元素中的第二个。因此,如果你写 li + li {font-weight: bold;},每个列表中的第二和第三项将被加粗。第一项将不受影响,如 图 2-20 所示。

css5 0220

图 2-20. 选择相邻兄弟

要正确工作,CSS 要求两个元素按源顺序出现。在我们的例子中,<ol> 元素后面是一个 <ul> 元素。这使我们可以使用 ol + ul 选择第二个元素,但不能使用相同的语法选择第一个元素。为了匹配 ul + ol,有序列表必须紧跟在无序列表后面。

请注意,两个元素之间的文本内容不会阻止相邻兄弟选择器的工作。考虑这个标记片段,其树视图与 图 2-18 中显示的相同。

<div>
  <ol>
    <li>List item 1</li>
    <li>List item 1</li>
    <li>List item 1</li>
  </ol>
  This is some text that is part of the 'div'.
  <ul>
    <li>A list item</li>
    <li>Another list item</li>
    <li>Yet another list item</li>
  </ul>
</div>

即使两个列表之间有文本,我们仍然可以使用选择器 ol + ul 匹配第二个列表。这是因为介于两个列表之间的文本不是兄弟元素的一部分,而是父 <div> 的一部分。如果我们将该文本包装在段落元素中,则会阻止 ol + ul 匹配第二个列表。相反,我们可能需要编写类似 ol + p + ul 的内容。

如下例所示,相邻兄弟选择器可以与其他组合器一起使用:

html > body table + ul{margin-top: 1.5em;}

选择器的翻译是:“选择任何紧随 <body> 元素的 <html> 元素的子元素 <table> 的兄弟元素 <ul>。”

与所有组合器一样,您可以将相邻兄弟选择器放置在更复杂的环境中,例如 div#content h1 + div ol。这个选择器的解释是:“选择任何 <ol> 元素,当 <div><h1> 的相邻兄弟,并且 <div> 自身是具有值为 contentid 属性的 <div> 的后代时。”

选择后续兄弟元素

通用兄弟选择器允许您在两个共享相同父元素的元素中选择后面的任何元素,表示为波浪符(~)组合器。

例如,要将跟在 <h2> 后且与 <h2> 共享父元素的任何 <ol> 斜体化,您可以编写 h2 ~ ol {font-style: italic;}。这两个元素不必是相邻兄弟,虽然它们可以相邻并仍然匹配此规则。将此规则应用于下面的标记的结果显示在 图 2-21 中:

<div>
  <h2>Subheadings</h2>
  <p>It is the case that not every heading can be a main heading.  Some headings
  must be subheadings.  Examples include:</p>
  <ol>
    <li>Headings that are less important</li>
    <li>Headings that are subsidiary to more important headlines</li>
    <li>Headings that like to be dominated</li>
  </ol>
  <p>Let's restate that for the record:</p>
  <ol>
    <li>Headings that are less important</li>
    <li>Headings that are subsidiary to more important headlines</li>
    <li>Headings that like to be dominated</li>
  </ol>
</div>

如您所见,两个有序列表都是斜体的。这是因为它们都是与它们共享父元素(<div>)的 <h2> 元素。

css5 0221

图 2-21. 选择后续兄弟元素

总结

通过基于文档语言的选择器,您可以创建适用于大量相似元素的 CSS 规则,就像构建适用于非常特定情况的规则一样简单。将选择器和规则组合在一起可以使样式表既紧凑又灵活,这意味着文件大小更小,下载速度更快。

选择器通常是用户代理必须正确掌握的内容,因为无法正确解释选择器几乎会完全阻止用户代理使用 CSS。另一方面,作者正确编写选择器非常关键,因为错误可能会导致用户代理无法按预期应用样式。正确理解选择器及其如何组合的一个重要部分是深入理解选择器与文档结构的关系,以及在确定元素样式时如何运用继承和级联机制。

本章涵盖的选择器并非全部,甚至不到一半。在下一章中,我们将深入探讨伪类和伪元素选择器这个功能强大且不断扩展的世界。

第三章:伪类和伪元素选择器

在前一章中,你看到了如何使用相对简单的表达式来匹配文档中的单个元素或元素集合,这些表达式匹配 HTML 文档中的属性。如果你的需求只是基于属性进行样式设置,这些方法非常适用。但如果你需要根据文档当前的状态或结构来设置文档的一部分样式,或者如果你想选择所有被禁用的表单元素,或者必须要求表单提交的元素,那么 CSS 就有了伪类和伪元素选择器,它们可以实现这些功能,以及更多。

伪类选择器

伪类选择器 允许你为实际上通过某些元素的状态或文档中的标记模式,甚至文档本身的状态推断出的幻影类分配样式。

术语 幻影类 可能听起来有点奇怪,但这确实是思考伪类工作方式的最佳方式。例如,假设你想突出显示数据表的每一行。你可以通过将每一行标记为 class="even",然后编写 CSS 来突出显示具有该类的行,或者(正如你很快会看到的)你可以使用伪类选择器来实现相同的效果,它会像你已经将所有这些类添加到标记中一样操作,尽管你实际上并没有这样做。

这里需要明确一个伪类的一个方面:伪类总是指它们附加到的元素,而不是其他的。听起来像是显而易见的事情,对吧?之所以明确说明这一点,是因为对于一些伪类来说,错误地认为它们是指的后代元素是一个常见的错误。

为了说明这一点,Eric 想分享一个个人轶事:

当我 2003 年第一个孩子出生时,我按照惯例在网上宣布了这个消息。很多人发来祝贺的同时也开了些 CSS 的笑话,其中主要的是选择器 #ericmeyer:first-child(稍后我们会详细讨论 :first-child)。但是那个选择器会选择我,而不是我的女儿,只有在我自己父母的第一个孩子时(事实上,我确实是)。要正确选择我的第一个孩子,那个选择器应该是 #ericmeyer > :first-child

这种混淆是可以理解的,这也是我们在这里讨论它的原因。在接下来的章节中会有提醒。请记住,伪类的效果是将一种幻影类应用于它们附加到的元素,只要牢记这一点,你应该没问题。

所有的伪类,毫无例外,都是一个单冒号(:)前面跟着一个词或连字符的术语,并且它们可以出现在选择器的任何位置。

结合伪类

在我们真正开始之前,关于链接的一点。CSS 允许将伪类结合(链)。例如,你可以使未访问的链接在悬停时变红色,并在悬停时访问的链接变为栗色:

a:link:hover {color: red;}
a:visited:hover {color: maroon;}

指定的顺序无关紧要;您也可以写a:hover:link,效果与a:link:hover相同。还可以为其他语言中的未访问和已访问链接分配单独的悬停样式,例如德语:

a:link:hover:lang(de) {color: gray;}
a:visited:hover:lang(de) {color: silver;}

注意不要组合互斥的伪类。例如,链接不能同时是访问过和未访问过的,所以a:link:visited毫无意义,并且永远不会匹配任何内容。

结构性伪类

我们将首先探讨的伪类是结构性的;也就是说,它们涉及文档的标记结构。大多数伪类依赖于标记内的模式,比如选择每第三个段落,但其他伪类允许您针对特定类型的元素进行选择。

选择根元素

这是结构简单性的精髓:伪类:root选择文档的根元素。在 HTML 中,这总是<html>元素。此选择器的真正好处在于为 XML 语言编写样式表时,根元素可能在每种语言中都不同——例如,在 SVG 中是<svg>元素,在我们之前的 PlanetML 示例中是<pml>元素——甚至在同一语言中可能有多个可能的根元素(尽管不是在单个文档中!)。

下面是在 HTML 中样式化根元素的示例,如图 3-1 所示:

:root {border: 10px dotted gray;}
body {border: 10px solid black;}

css5 0301

图 3-1. 样式化根元素

在 HTML 文档中,您可以直接选择<html>元素,而不必使用:root伪类。这两个选择器在特异性方面有所不同,我们将在第四章中进行详细讨论,但除此之外,它们效果相同。

选择空元素

使用伪类:empty,您可以选择任何没有任何子元素(包括文本节点)的元素,这包括文本和空白。这在抑制内容管理系统(CMS)生成的未填充任何实际内容的元素时非常有用。因此,p:empty {display: none;}将阻止显示任何空段落。

请注意,为了匹配,元素必须从解析的角度来看确实为空——没有空白、可见内容或后代元素。以下元素中,只有第一个和最后一个会被p:empty匹配到:

<p></p>
<p> </p>
<p>
</p>
<p><!—-a comment--></p>

第二和第三段落不会被:empty匹配,因为它们不是空的:分别包含一个空格和一个换行符。它们都被视为文本节点,因此阻止了空状态。最后一个段落匹配,因为注释不被视为内容,甚至不是空白。但是,只要在注释两侧放置一个空格或换行符,p:empty就无法匹配。

你可能会被诱惑只是样式化所有空元素,比如 *:empty {display: none;},但其中有一个隐蔽的陷阱::empty 匹配 HTML 的空元素,比如 <img><hr><br><input>。甚至可以匹配 <textarea>,除非你在 <textarea> 元素中插入一些默认文本。

因此,在匹配元素方面,imgimg:empty 是等效的(它们在特异性上有所不同,这将在下一章节中讨论)。

选择仅为子元素

如果你曾想选择所有被超链接元素包裹的图片,:only-child 伪类正是为你而设计的。它在元素是另一个元素的唯一子元素时进行选择。比如说你想给任何一个作为其父元素唯一子元素的图片加上边框,你可以写成以下这样:

img:only-child {border: 1px solid black;}

这将匹配符合这些条件的任何图片。因此,如果你有一个段落包含一张图片且没有其他子元素,那么无论周围有多少文本,该图片都会被选中。如果你真正想要的是仅为唯一子元素且位于超链接内的图片,你只需修改选择器如下(如图 3-2 中所示):

a[href] img:only-child {border: 2px solid black;}
<a href="http://w3.org/"><img src="w3.png" alt="W3C"></a>
<a href="http://w3.org/"><img src="w3.png" alt=""> The W3C</a>
<a href="http://w3.org/"><img src="w3.png" alt=""> <em>The W3C</em></a>

css5 0302

图 3-2. 选择只作为链接内唯一子元素的图片

你应该记住关于 :only-child 的两件事。首先是你总是将其应用于你希望成为唯一子元素的元素,而不是父元素,正如前面所解释的那样。这带来的第二件事要记住的是,当你在后代选择器中使用 :only-child 时,你并没有将列出的元素限制为父子关系。

回到超链接图片的例子,a[href] img:only-child 匹配任何作为其直接父元素的唯一子元素且是 a 元素后代的图片,无论它是否是 a 元素的子元素。要匹配,元素图片必须是其直接父元素的唯一子元素,同时也是具有 href 属性的 a 元素的后代,但该父元素本身可以是相同 <a> 元素的后代。因此,以下三个图片都会被匹配,如图 3-3 所示:

a[href] img:only-child {border: 5px solid black;}
<a href="http://w3.org/"><img src="w3.png" alt="W3C"></a>
<a href="http://w3.org/"><span><img src="w3.png" alt="W3C"></span></a>
<a href="http://w3.org/">A link to <span>the <img src="w3.png" alt="W3C">
   web</span> site</a>

css5 0303

图 3-3. 选择只作为链接内唯一子元素的图片,再论

在每种情况下,图片都是其父元素的唯一子元素,并且也是 <a> 元素的后代。因此,所有三个图片都被所示规则匹配。如果你想限制规则以使其匹配作为 <a> 元素唯一子元素的图片,你可以添加子元素结合器,以得到 a[href] > img:only-child。通过这种修改,只有第三章中显示的第一个图片会被匹配。

仅使用 :only-of-type 选择

这都很好,但是如果要匹配作为超链接中唯一图像的图像,但其他元素可能也在其中,该怎么办?考虑以下情况:

<a href="http://w3.org/"><b>•</b><img src="w3.png" alt="W3C"></a>

在这种情况下,我们有一个 a 元素,其有两个子元素:<b><img>。该图像不再是其父级(超链接)的唯一子元素,因此永远不会使用 :only-child 进行匹配。但是,它可以使用 :only-of-type 进行匹配。这在图 3-4 中有所说明:

a[href] img:only-of-type {border: 5px solid black;}
<a href="http://w3.org/"><b>•</b><img src="w3.png" alt="W3C"></a>
<a href="http://w3.org/"><span><b>•</b><img src="w3.png" alt="W3C"></span></a>

css5 0304

图 3-4。选择作为其类型唯一同级的图像

不同之处在于:only-of-type将匹配任何元素,该元素是其同级中唯一的此类型,而:only-child只会在元素根本没有同级时才匹配。

在某些情况下,这可以非常有用,例如在段落中选择图像而无需担心超链接或其他内联元素的存在:

p > img:only-of-type {float: right; margin: 20px;}

只要没有多个是同一段落的子图像,该图像将被浮动到右侧。

您还可以使用此伪类为文档中给定部分中的 <h2> 应用额外样式,如下所示:

section > h2 {margin: 1em 0 0.33em; font-size: 1.8rem; border-bottom: 1px solid
   gray;}
section > h2:only-of-type {font-size: 2.4rem;}

根据这些规则,任何只有一个子 <h2><section> 将使该 <h2> 显示比平常大。如果 section 有两个或更多 <h2> 子元素,则不会有一个比另一个更大。其他子元素的存在——无论是其他标题级别、表格、段落、列表等等——都不会影响匹配。

还有一点需要澄清的是:only-of-type只引用元素,不引用其他内容。考虑以下情况:

p.unique:only-of-type {color: red;}
<div>
  <p class="unique">This paragraph has a 'unique' class.</p>
  <p>This paragraph doesn't have a class at all.</p>
</div>

在这种情况下,这两个段落都不会被选中。为什么?因为两个段落都是 <div> 的后代,它们中的任何一个都不能是其类型的唯一一个。

这里类名无关紧要。我们可能会被误导,认为type是一个泛泛的描述,因为我们解析语言的方式。然而,:only-of-type 所指的type只指元素类型,就像类型选择器一样。因此,p.unique:only-of-type 意味着“选择任何 <p> 元素,该元素在其同级中是唯一的 <p> 元素,如果它还具有 classunique。” 它意味着“当 class 属性包含单词 unique 时,选择任何 <p> 元素,它是唯一一个符合该标准的同级段落。”

选择第一个子元素

常见的情况是想要对元素的第一个或最后一个子元素应用特殊样式。一个典型的例子是在选项卡栏中为一组导航链接应用样式,并希望在第一个或最后一个选项卡(或两者)上加上特殊的视觉效果。如果没有结构选择器,可以通过为这些元素应用特殊类来实现。我们有伪类来为我们承担这个任务,无需手动确定哪些元素是第一个和最后一个。

伪类:first-child用于选择作为其他元素第一个子元素的元素。考虑以下标记:

<div>
  <p>These are the necessary steps:</p>
  <ul>
    <li>Insert key</li>
    <li>Turn key <strong>clockwise</strong></li>
    <li>Push accelerator</li>
  </ul>
  <p>
    Do <em>not</em> push the brake at the same time as the accelerator.
  </p>
</div>

在此示例中,第一个子元素是第一个<p>、第一个<li>以及<strong><em>元素,它们都是其各自父元素的第一个子元素。根据以下两个规则,

p:first-child {font-weight: bold;}
li:first-child {text-transform: uppercase;}

我们得到如图 3-5 所示的结果。

css5 0305

图 3-5. 样式化第一个子元素

第一个规则使任何作为另一个元素的第一个子元素的<p>元素加粗。第二个规则将使作为另一个元素的第一个子元素的<li>元素大写(在 HTML 中,这必须是<ol><ul>元素)。

正如已经提到的,最常见的错误是假设像p:first-child这样的选择器将选择<p>元素的第一个子元素。请记住伪类的本质,即将一种幻影类附加到锚点元素,即与伪类相关联的元素。如果要向标记添加实际类,它看起来会像这样:

<div>
  <p class="first-child">These are the necessary steps:</p>
  <ul>
    <li class="first-child">Insert key</li>
    <li>Turn key <strong class="first-child">clockwise</strong></li>
    <li>Push accelerator</li>
  </ul>
  <p>
    Do <em class="first-child">not</em> push the brake at the same time as the
  accelerator.
  </p>
</div>

因此,如果您想选择那些作为另一个元素的第一个子元素的<em>元素,则应编写em:first-child

选择最后的子元素

:first-child的镜像是:last-child。如果我们采用前面的示例并只更改伪类,则得到如图 3-6 所示的结果:

p:last-child {font-weight: bold;}
li:last-child {text-transform: uppercase;}
<div>
  <p>These are the necessary steps:</p>
  <ul>
    <li>Insert key</li>
    <li>Turn key <strong>clockwise</strong></li>
    <li>Push accelerator</li>
  </ul>
  <p>
    Do <em>not</em> push the brake at the same time as the accelerator.
  </p>
</div>

css5 0306

图 3-6. 样式化最后的子元素

第一个规则使任何作为另一个元素的最后一个子元素的<p>元素加粗。第二个规则将使作为另一个元素的最后一个子元素的<li>元素大写。如果您想要选择最后一个段落中的<em>元素,则可以使用选择器p:last-child em,它选择任何作为自身另一个元素的最后一个子元素的<p>元素中的<em>元素。

有趣的是,您可以结合这两个伪类来创建一个only-child的版本。以下两个规则将选择相同的元素:

p:only-child {color: red;}
p:first-child:last-child {background-color: red;}

无论哪种方式,我们得到具有红色前景和背景颜色的段落(明确地说,这不是一个好主意)。

选择类型的第一个和最后一个

与选择元素的第一个和最后一个子元素类似,您可以选择在另一个元素内部的特定类型的元素的第一个或最后一个。这允许执行诸如选择给定元素内的第一个<table>之类的操作,而不管之前有什么元素:

table:first-of-type {border-top: 2px solid gray;}

请注意,这不适用于整个文档;所示的规则不会选择文档中的第一个表并跳过所有其他表。它将选择每个包含表的元素内的第一个<table>元素,并跳过任何在第一个之后的同级<table>元素。因此,给定图 3-7 中显示的文档结构,被圈出的节点是被选择的节点。

css5 0307

图 3-7. 选择第一个类型为表的表格

在表的上下文中,一种有用的方式是选择行中第一个数据单元格,无论该行中是否有一个标题单元格在它之前:

td:first-of-type {border-left: 1px solid red;}

这将选择每个表行中的第一个数据单元格(即包含7R的单元格):

<tr>
  <th scope="row">Count</th><td>7</td><td>6</td><td>11</td>
</tr>
<tr>
  <td>R</td><td>X</td><td>-</td>
</tr>

与选择td:first-child的效果进行比较,它将选择第二行中的第一个<td>元素,但不会选择第一行中的第一个<td>元素。

另一面是:last-of-type,它从其兄弟元素中选择给定类型的最后一个实例。在某种程度上,它就像:first-of-type,不同之处在于你从一组兄弟姐妹的最后一个元素开始,向后遍历直到到达该类型的第一个实例。在图 3-8 中显示的文档结构中,用table:last-of-type选择的是圈出来的节点。

css5 0308

图 3-8. 选择最后一个类型为表的表格

正如在:only-of-type中所指出的,记住你选择的是同类元素中的元素,而不是在整个文档中同一类型的所有元素的第一个(或最后一个)作为单一组。每组共享父元素的元素被视为单独的一组,你可以选择每组中类型的第一个(或最后一个)。

与前一节提到的类似,你可以结合这两个伪类创建:only-of-type的版本。以下两个规则将选择相同的元素:

table:only-of-type{color: red;}
table:first-of-type:last-of-type {background: red;}

选择每个第 n 个子元素

如果你可以选择其他元素的第一个、最后一个或唯一的子元素,那么如何选择每第三个子元素?所有偶数子元素?只有第九个子元素?CSS 不是试图定义无数个命名伪类,而是有:nth-child()伪类。通过在括号内填写整数或甚至基本的代数表达式,你可以选择任何你喜欢的任意编号子元素。

让我们从:first-child:nth-child()等效部分:nth-child(1)开始。在下面的例子中,所选元素将是第一个段落和第一个列表项:

p:nth-child(1) {font-weight: bold;}
li:nth-child(1) {text-transform: uppercase;}
<div>
  <p>These are the necessary steps:</p>
  <ul>
    <li>Insert key</li>
    <li>Turn key <strong>clockwise</strong></li>
    <li>Push accelerator</li>
  </ul>
  <p>
    Do <em>not</em> push the brake at the same time as the accelerator.
  </p>
</div>

但是,如果我们把数字从1改为2,则不会选择任何段落,并且将选择中间(第二个)列表项,如在图 3-9 中所示:

p:nth-child(2) {font-weight: bold;}
li:nth-child(2) {text-transform: uppercase;}

css5 0309

图 3-9. 设置第二个子元素样式

你可以插入任何你选择的整数。如果你有一个使用案例,需要选择任何父元素的第 93 个有序列表子元素,ol:nth-child(93)可以随时为你服务。只要该子元素是有序列表,它就会匹配任何父元素的第 93 个子元素。(这并不意味着在其兄弟姐妹中的第 93 个有序列表;参见“选择每种类型的第 n 个”)

使用:nth-child(1)而不是:first-child有何理由?没有。在这种情况下,可以使用任何您喜欢的。它们之间实际上没有区别。

更强大的是,您可以使用简单的代数表达式形式an + ban - b来定义重复的实例,其中ab是整数,n表示为其本身。此外,+ b b部分是可选的,因此如果不需要,可以省略。

假设我们想要选择无序列表中的每第三个列表项,从第一个开始。以下操作使其成为可能,选择第一个和第四个项目,如图 3-10 所示:

ul > li:nth-child(3n + 1) {text-transform: uppercase;}

css5 0310

图 3-10。样式化每第三个列表项

这种工作方式是n表示系列 0、1、2、3、4 等直到无限。然后浏览器解决3n + 1,产生 1、4、7、10、13 等。如果删除+ 1,即简单留下3n,结果将是 0、3、6、9、12 等。由于没有第 0 个列表项——所有元素计数从 1 开始,这个表达式选中的第一个列表项将是列表中的第三个列表项。

鉴于元素计数从 1 开始,推断:nth-child(2n)将选择偶数编号的子元素,而:nth-child(2n+1):nth-child(2n-1)将选择奇数编号的子元素是一种小技巧。您可以将其记忆下来,或者您可以使用:nth-child()接受的两个特殊关键字:evenodd。想要突出显示表的每一行中的其他行,从第一行开始?以下是如何实现的,结果显示在图 3-11 中:

tr:nth-child(odd) {background: silver;}

css5 0311

图 3-11。样式化每隔一行的表行

任何比每隔一个元素更复杂的东西都需要an + b表达式。

请注意,在b使用负数时,必须去掉+号,否则选择器将完全失败。以下两个规则中,只有第一个会起作用。解析器将丢弃第二个,并完全忽略整个声明块:

tr:nth-child(4n - 2) {background: silver;}
tr:nth-child(3n + −2) {background: red;}  /* INVALID */

您还可以在表达式中使用负的a值,这将有效地从您在b中使用的术语开始向后计数。选择列表中的前五个列表项可以这样做:

li:nth-child(-n + 5) {font-weight: bold;}

这有效是因为负数n为 0、–1、–2、–3、–4 等。对每个数加 5,您会得到 5、4、3、2、1 等。为n添加负数乘数,您可以选择每第二个、第三个或您想要的任何编号的元素,如下所示:

li:nth-child(-2n + 10) {font-weight: bold;}

这将选择列表中的第 10、第 8、第 4 和第 2 个列表项。

正如您可能期望的那样,对应的伪类是nth-last-child()。这让您可以像使用nth-child()一样操作,只不过nth-last-child()从同级元素列表的最后一个元素开始向前计数。如果您打算突出显示每隔一个表格行确保最后一行是突出显示模式中的一行,则这两者都适合您:

tr:nth-last-child(odd) {background: silver;}
tr:nth-last-child(2n+1) {background: silver;} /* equivalent */

如果文档对象模型(DOM)更新以添加或删除表格行,则无需添加或删除类。通过使用结构选择器,这些选择器将始终匹配更新后的 DOM 的奇数行。

如果符合条件,可以使用nth-child()nth-last-child()匹配任何元素。考虑这些规则,其结果显示在图 3-12 中。

li:nth-child(3n + 3) {border-left: 5px solid black;}
li:nth-last-child(4n - 1) {border-right: 5px solid black; background: silver;}

再次,对于a使用负数术语实际上是在倒数计数,但由于这个伪类已经从末尾开始计数,负数术语实际上是正向计数。也就是说,你可以这样选择列表中的最后五个列表项:

li:nth-last-child(-n + 5) {font-weight: bold;}
注意

nth-child()nth-last-child()的扩展允许从由简单或复合选择器匹配的元素中进行选择;例如,nth-child(2n + 1 of p.callout)。截至 2023 年初,这在 Safari 和 Chrome 的测试版中得到支持。随着其包含在 Interop 2023 中,计划在不久的将来完全支持它。

css5 0312

图 3-12. 结合nth-child()nth-last-child()的模式

您还可以将这两个伪类连接在一起,如nth-child(1):nth-last-child(1),从而创建一个更详细的only-child的重新陈述。除了创建具有更高特异性的选择器(详见第四章)外,没有真正的理由这样做,但这是一个选项。

您可以使用 CSS 确定列表中项目的数量,并相应地设置其样式:

li:only-child {width: 100%;}
li:nth-child(1):nth-last-child(2),
li:nth-child(2):nth-last-child(1) {width: 50%;}
li:nth-child(1):nth-last-child(3),
li:nth-child(1):nth-last-child(3) ~ li {width: 33.33%;}
li:nth-child(1):nth-last-child(4),
li:nth-child(1):nth-last-child(4) ~ li {width: 25%;}

在这些示例中,如果列表项是唯一的列表项,则宽度为 100%。如果列表项既是第一项又是倒数第二项,这意味着有两个项目,宽度为 50%。如果一项既是第一项又是倒数第三项,我们将它及其后续的两个同级列表项设为 33%的宽度。同样,如果列表项既是第一项又是倒数第四项,这意味着正好有四个项目,因此我们将它及其三个同级项设为宽度的 25%。(注意:使用:has()伪类可以更轻松地实现这种效果,详见“:has() 伪类”。)

选择每种类型的第 n 个

如同可能已经成为熟悉的模式一样,:nth-child():nth-last-child() 伪类在 :nth-of-type():nth-last-of-type() 中有类似物。例如,您可以选择给定段落的每隔一个的超链接,从第二个开始使用 p > a:nth-of-type(even)。这将忽略所有其他元素(<span><strong> 等),并仅考虑链接,如图 3-13 所示:

p > a:nth-of-type(even) {background: blue; color: white;}

css5 0313

图 3-13. 选择偶数链接

如果您想要从最后一个超链接向后工作,则可以使用p > a:nth-last-of-type(even)

与以前一样,这些伪类从其兄弟元素中选择元素的类型,而不是从整个文档中同一类型的所有元素中选择。每个元素都有其自己的兄弟元素列表,并且选择发生在每个组内部。

:nth-of-type()nth-child() 的区别在于,:nth-of-type() 计算您选择的实例数,并在该元素集合内进行计数。例如,考虑以下标记:

<tr>
   <th scope="row">Count</th>
   <td>7</td>
   <td>6</td>
   <td>11</td>
   <td>17</td>
   <td>3</td>
   <td>21</td>
</tr>
<tr>
   <td>R</td>
   <td>X</td>
   <td>-</td>
   <td>C</td>
   <td>%</td>
   <td>A</td>
   <td>I</td>
</tr>

如果您想要选择行中位于偶数列中的每个表格单元格,则应使用td:nth-child(even)。但是,如果您想要选择表格单元格的每个偶数实例,则应使用td:nth-of-type(even)。您可以在图 3-14 中看到此差异,该图展示了以下 CSS 的结果:

td:nth-child(even) {background: silver;}
td:nth-of-type(even) {text-decoration: underline;}

css5 0314

图 3-14. 选择nth-childnth-of-type表格单元格

在第一行中,选择每个偶数序号的表格数据单元格(td),从表头单元格(th)后的第一个单元格开始。在第二行中,由于所有单元格都是td单元格,这意味着该行中的所有单元格都是相同类型的,因此计数从第一个单元格开始。

正如您可能期望的那样,您可以一起使用:nth-of-type(1):nth-last-of-type(1)来重申:only-of-type,但具有更高的特异性。(我们在第四章中解释特异性,我们保证。)

位置伪类

使用位置伪类,我们进入了根据文档结构以外的东西匹配文档部分的选择器领域——这些东西仅仅通过研究文档的标记是无法准确推断的。

这可能听起来像是随机应用样式,但并非如此。相反,我们根据一些无法预先预测的相对短暂的条件来应用样式。然而,样式将出现的具体情况确实是明确定义的。

请这样想:在体育赛事中,当主队得分时,观众会欢呼。你不知道球赛中主队将何时得分,但当他们得分时,观众会像预测的那样欢呼。无法预测欢呼的确切时刻并不使其显得不可预测。

现在考虑锚点元素 (<a>),它(在 HTML 和相关语言中)建立了从一个文档到另一个文档的链接。锚点始终是锚点,但有些锚点指向已经访问过的页面,而其他一些则指向尚未访问过的页面。通过简单查看 HTML 标记,你无法区分它们之间的差异,因为在标记中,所有的锚点看起来都是相同的。

唯一可以知道哪些链接已被访问的方法是将文档中的链接与用户的浏览历史进行比较。因此,实际上有两种基本类型的链接:已访问和未访问。

针对超链接的伪类

CSS 定义了一些仅适用于超链接的伪类。在 HTML 中,超链接是任何带有 href 属性的 <a> 元素;在 XML 语言中,超链接是任何充当到另一个资源的链接的元素。表 3-1 描述了你可以应用于它们的伪类。

表 3-1. 链接伪类

名称 描述
:link 指向任何超链接(即具有 href 属性)且指向未访问地址的锚点。
:visited 指向已访问地址的超链接的锚点。出于安全原因,可应用于已访问链接的样式严格受限;详细信息请参阅“访问链接和隐私”。
:any-link 指向由 :link:visited 匹配的任何元素。
:local-link 指向与被样式化页面相同 URL 的任何链接。例如文档内的跳转链接。注:截至 2023 年初尚不支持

表 3-1 中的第一个伪类看起来有点多余。毕竟,如果一个锚点尚未被访问,它必然是未访问的,对吧?如果是这样,我们所需的只是以下内容:

a {color: blue;}
a:visited {color: red;}

尽管这种格式看起来很合理,但还不够。这里展示的规则中的第一个不仅适用于未访问的链接,而且适用于任何没有 href 属性的 <a> 元素,例如这个:

<a id="section004">4\. The Lives of Meerkats</a>

由于 <a> 元素将匹配规则 a {color: blue;},因此结果文本将是蓝色的。因此,为了避免将您的链接样式应用于占位符链接,请使用 :link:visited 伪类:

a:link {color: blue;}    /* unvisited links are blue */
a:visited {color: red;}   /* visited links are red */

这是一个重新审视属性和类选择器,并展示它们如何与伪类结合的好地方。例如,假设你想要改变指向你自己站点外部的链接的颜色。在大多数情况下,我们可以使用以某个属性值开头的属性选择器。然而,有些内容管理系统将所有链接设置为绝对 URL,这种情况下你可以给每个锚点分配一个类。这很容易:

<a href="/about.html">My About page</a>
<a href="https://www.site.net/" class="external">An external site</a>

要为外部链接应用不同的样式,你只需像这样设置一个规则:

a.external:link, a[href^="http"]:link { color: slateblue;}
a.external:visited, a[href^="http"]:visited  {color: maroon;}

此规则将默认使前述标记中的第二个锚点呈钢蓝色,并且一旦被访问将变为栗色,而第一个锚点将保持超链接的默认颜色(通常未访问时为蓝色,已访问时为紫色)。为了提高可用性和无障碍性,应该清晰地区分访问过的链接和未访问的链接。

注意

样式化访问过的链接使访问者知道他们已经访问过的页面以及尚未访问的页面。在大型网站上,这尤为重要,因为对于那些有认知障碍的人来说,记住已访问页面可能会很困难。突出显示访问过的链接不仅是 W3C 网页内容无障碍指南的一部分,而且可以使搜索内容更快捷、更高效,减少压力。

ID 选择器使用相同的一般语法:

a#footer-copyright:link {background: yellow;}
a#footer-copyright:visited {background: gray;}

如果你想选择所有链接,不论其是否已访问,可以使用 :any-link

a#footer-copyright:any-link {text-decoration: underline;}

非超链接位置伪类

超链接并不是唯一与位置相关的元素。CSS 还提供了一些与超链接目标相关的伪类,总结在 表 3-2 中。

表 3-2. 非超链接位置伪类

名称 描述
:target 指的是其id属性值与 URL 中的片段选择器匹配的元素——即 URL 特别指定的元素。
:target-within 指的是 URL 的目标元素,或者包含被目标元素选定的元素。注意:截至 2023 年初还不支持。
:scope 指的是作为选择器匹配参考点的元素。

让我们谈谈目标选择。当 URL 包含片段标识符时,它指向的文档部分在 CSS 中称为“目标”。因此,你可以使用 :target 伪类唯一地为任何片段标识符目标的元素设置样式。

即使你对“片段标识符”这个术语不熟悉,你可能已经在使用中见过它们。考虑这个 URL:

http://www.w3.org/TR/css3-selectors/#target-pseudo

URL 的 target-pseudo 部分是片段标识符,由 # 符号标记。如果引用页面(http://www.w3.org/TR/css3-selectors/)具有 target-pseudo ID 的元素,则该元素成为片段标识符的目标。

感谢:target,您可以突出显示文档中的任何目标元素,或者您可以为可能被定位的各种类型的元素设计不同的样式,比如为定位的标题设计一种样式,为定位的表格设计另一种样式等等。图 3-15 展示了:target的示例:

*:target {border-left: 5px solid gray; background: yellow url(target.png)
    top right no-repeat;}

css5 0315

图 3-15。样式化片段标识符目标

:target样式不会应用于三种情况:

  • 通过不带片段标识符的 URL 访问页面。

  • 通过带有片段标识符的 URL 访问页面,但标识符与文档中的任何元素都不匹配。

  • 页面的 URL 以不创建滚动状态的方式更新,这通常通过 JS 花招实现。(这不是 CSS 规则,但这是浏览器的行为。)

更有趣的是,如果文档中的多个元素可以通过片段标识符匹配,例如,如果作者错误地在同一文档中包含三个单独的<div id="target-pseudo">实例会发生什么?

简而言之,CSS 没有也不需要覆盖这种情况的规则,因为 CSS 只关注样式目标。无论浏览器选择三个元素中的一个作为目标,还是将三个元素都指定为相等的目标,:target样式应该应用于任何有效的目标。

:target伪类密切相关的是:target-within伪类。不同之处在于:target-within不仅匹配目标元素,还匹配目标元素的祖先元素。因此,以下 CSS 将匹配包含目标的任何<p>元素,或者本身是目标的任何元素:

p:target-within {border-left: 5px solid gray; background: yellow url(target.png)
    top right no-repeat;}

或者,如果任何浏览器支持的话。截至 2023 年初,情况并非如此。

最后,我们考虑:scope伪类。这在很大程度上得到了支持,但目前只在脚本情况下很有用。考虑以下 JS 和 HTML,我们将在代码后解释:

var output = document.getElementById('output');
var registers = output.querySelectorAll(':scope > div');
<section id="output">
  <h3>Results</h3>
  <div></div>
  <div></div>
</section>

JS 部分实际上是说:“找到 ID 为output的元素。然后,找到刚刚找到的output元素的所有<div>子元素。”(是的,CSS 选择器可以在 JS 中使用!)那段 JS 中的:scope指的是已找到的事物的范围,因此将选择限制在其中而不是整个文档。结果是,在 JS 程序的内存中,现在有一个结构保存对 HTML 中两个<div>元素的引用。

如果您在纯 CSS 中使用:scope,它将指向作用域根,目前(假设文档是 HTML)意味着<html>元素。HTML 和 CSS 都没有提供设置作用域根的方法,除了文档的根元素。因此,在 JS 之外,:scope本质上等同于:root。这可能会在未来发生变化,但目前,您应该只在 JS 上下文中使用:scope

用户操作伪类

CSS 定义了一些伪类,可以根据用户的操作改变文档的外观。这些 动态伪类 传统上用于样式化超链接,但可能性更广泛。伪类在 表 3-3 中描述。

表 3-3. 用户操作伪类

名称 描述
:hover 指鼠标指针悬停在其上的任何元素,例如,鼠标指针悬停在其上的超链接
:active 指任何被用户输入激活的元素,例如,用户在按住鼠标按钮的时间内单击的超链接,或者用户通过触摸屏轻拍的元素
:focus 指当前具有输入焦点的任何元素,即可以接受键盘输入或以某种方式被激活的元素
:focus-within 指当前具有输入焦点的任何元素,即可以接受键盘输入或以某种方式被激活的元素,或包含具有这种焦点的元素的元素
:focus-visible 指当前具有输入焦点的任何元素,但仅当用户代理认为它是应该具有可见焦点的元素类型时

可以变为 :active 或具有 :focus 的元素包括链接、按钮、菜单项、任何具有 tabindex 值的元素以及所有其他交互式元素,包括表单控件和包含可编辑内容的元素(通过在元素的开放标签中添加 contenteditable 属性)。

:link:visited 一样,这些伪类在超链接的上下文中最为熟悉。许多网页的样式看起来像这样:

a:link {color: navy;}
a:visited {color: gray;}
a:focus {color: orange;}
a:hover {color: red;}
a:active {color: yellow;}
注意

伪类的顺序比起初看起来更为重要。通常的建议是 linkvisitedfocushoveractive。下一章解释了为什么这个特定顺序很重要,并讨论了您可能选择改变甚至忽略建议的几个原因。

注意动态伪类可以应用于任何元素,这很有用,因为经常需要对不是链接的元素应用动态样式。考虑这个例子:

input:focus {background: silver; font-weight: bold;}

通过使用这个标记,您可以突出显示准备接受键盘输入的表单元素,如 图 3-16 所示。

css5 0316

图 3-16. 突出显示具有焦点的表单元素

用户操作伪类的两个相对较新的补充是 :focus-within:focus-visible。让我们先看第二个。

:focus-visible 伪类

:focus-visible 类与 :focus 非常相似,它应用于具有焦点的元素,但有一个重要的区别:它仅在具有焦点的元素是用户代理认为在特定情况下应给予可见焦点样式的元素时匹配。

例如,考虑 HTML 按钮。当通过鼠标点击按钮时,该按钮获得焦点,就像我们使用键盘界面将焦点移动到它时一样。作为关注无障碍性和美观的作者,我们希望按钮在通过键盘或其他辅助技术获得焦点时具有焦点,但在通过点击或轻触时不要应用焦点样式。

我们可以通过使用以下 CSS 来解决这个问题:

button:focus-visible {outline: 5px solid maroon;}

当通过键盘切换到按钮时,它将被描绘为带有厚重的深红色轮廓,但当通过鼠标点击按钮时,规则不会被应用。

:focus-within 伪类

在此基础上,:focus-within适用于任何具有焦点的元素,或任何具有焦点后代的元素。根据以下的 CSS 和 HTML,我们将得到图 3-17 中显示的结果:

nav:focus-within {border: 3px solid silver;}
a:focus-visible {outline: 2px solid currentcolor;}
<nav>
  <a href="home.html">Home</a>
  <a href="about.html">About</a>
  <a href="contact.html">Contact</a>
</nav>

css5 0317

图 3-17. 使用:focus-within选择元素

当前第三个链接拥有焦点,用户通过按 Tab 键到达该链接,并以 2 像素的轮廓样式进行装饰。包含该链接的<nav>元素也通过:focus-within获得焦点样式,因为它的一个后代元素(即从它衍生的元素)当前拥有焦点。这为页面的该区域增加了一些视觉重量,这可能会有所帮助。但请注意不要过度使用焦点样式,因为过多的焦点样式可能会造成视觉过载,潜在地导致用户混淆。

警告

尽管您可以按自己的喜好为元素设置:focus样式,但不要从焦点元素中删除所有样式。区分当前具有焦点的元素对于无障碍性至关重要,特别是对于通过键盘或其他辅助技术导航您的站点或应用程序的用户。

动态样式化的现实世界问题

动态伪类呈现了一些有趣的问题和特殊性。例如,您可以将访问过的和未访问过的链接设置为一种字体大小,并使悬停链接变大,正如图 3-18 所示:

a:link, a:visited {font-size: 13px;}
a:hover, a:active {font-size: 20px;}

css5 0318

图 3-18. 使用动态伪类更改布局

正如您所看到的,当鼠标指针悬停在锚点上时,用户代理会增大锚点的大小,或者依靠:active设置,当用户在触摸屏上触摸它时也会增大。因为我们正在更改影响行高的属性,因此支持此行为的用户代理必须在锚点处于悬停状态时重新绘制文档,这可能会强制重新布局其后的所有内容。

UI 状态伪类

与动态伪类密切相关的是用户界面(UI)状态伪类,这些伪类在表 3-4 中进行了总结。这些伪类允许根据诸如复选框之类的 UI 元素的当前状态进行样式设置。

表 3-4. UI 状态伪类

名称 描述
:enabled 指的是已启用的 UI 元素(如表单元素),即可以进行输入
:disabled 指的是已禁用的 UI 元素(如表单元素),即不能进行输入
:checked 指的是已被选中的单选按钮或复选框,可以是用户手动选择的,也可以是文档本身的默认选择
:indeterminate 指的是既未选中也未取消选中的单选按钮或复选框;此状态仅可通过 DOM 脚本设置,而不是通过用户输入
:default 指的是默认选中的单选按钮、复选框或选项
:autofill 指的是浏览器自动填充的用户输入
:placeholder-shown 指的是具有占位符(而非实际值)文本预填充的用户输入
:valid 指的是满足所有数据有效性要求的用户输入
:invalid 指的是未满足所有数据有效性要求的用户输入
:in-range 指的是其值在最小值和最大值之间的用户输入
:out-of-range 指的是其值低于控件允许的最小值或高于最大值的用户输入
:required 指的是必须设置值的用户输入
:optional 指的是不需要设置值的用户输入
:read-write 指的是用户可以编辑的用户输入
:read-only 指的是用户无法编辑的用户输入

尽管 UI 元素的状态可以通过用户操作来改变,例如用户勾选或取消勾选复选框,但 UI 状态伪类并非纯粹动态的,因为它们也会受文档结构或脚本的影响。

已启用和已禁用的 UI 元素

多亏了 DOM 脚本和 HTML,您可以将 UI 元素(或者一组 UI 元素)标记为已禁用。禁用的元素会显示出来,但用户无法选择、激活或与其交互。作者可以通过 DOM 脚本或者向元素的标记添加 disabled 属性来设置元素为禁用状态。

任何可以禁用但尚未禁用的元素,从定义上来说都是启用的。您可以使用 :enabled:disabled 伪类来为这两种状态设置样式。通常更常见的是为禁用元素设置样式,而保持启用元素不变,但两者都有其用途,如 图 3-19 所示:

:enabled {font-weight: bold;}
:disabled {opacity: 0.5;}

css5 0319

图 3-19. 样式化已启用和已禁用的 UI 元素

检查状态

除了启用或禁用,某些 UI 元素还可以被选中或未选中——在 HTML 中,输入类型checkboxradio符合这一定义。CSS 提供了:checked伪类来处理处于该状态的元素。此外,:indeterminate伪类匹配任何既不选中也不未选中的可检查 UI 元素。这些状态在图 3-20 中有说明:

:checked {background: silver;}
:indeterminate {border: red;}

css5 0320

图 3-20. 样式化选中和不定态 UI 元素

虽然可以在默认情况下将可检查元素设置为未选中状态,但 HTML 作者可以通过向元素的标记添加checked属性来切换它们的选中状态。作者还可以使用 DOM 脚本将元素的选中状态切换为选中或未选中状态,取决于他们的偏好。

自 2023 年初以来,只能通过 DOM 脚本或用户代理自身来设置不定态;不存在标记级别的方法来将元素设置为不定态。样式化不定态的目的在于视觉上指示用户需要检查(或取消检查)元素。然而,这只是一种视觉效果:它不会影响 UI 元素的基础状态,该状态根据文档标记和任何 DOM 脚本的影响可能是选中或未选中。

尽管前面的例子展示了样式化的单选按钮,但要记住,直接使用 CSS 样式化单选按钮和复选框实际上非常有限。尽管如此,这不应限制您使用已选择选项伪类的方式。例如,您可以通过结合:checked和相邻兄弟选择器来样式化与复选框和单选按钮关联的标签:

input[type="checkbox"]:checked + label {
  color: red;
  font-style: italic;
}
<input id="chbx" type="checkbox"> <label for="chbx">I am a label</label>

如果您需要选择所有未选中的复选框,请使用否定伪类(在本章后面有详细介绍),例如:input[type="checkbox"]:not(:checked)。只有单选按钮和复选框可以被选中。请注意,每个元素和这两个元素在未选中时都是:not(:checked)。这种方法不能填补缺失的:unchecked伪类的空白,应该仅匹配应该可检查的元素。

默认值伪类

三个伪类与默认值和填充文本有关::default:placeholder-shown:autofill

:default伪类匹配一组相似元素中的默认 UI 元素。这通常适用于上下文菜单项、按钮和选择列表/菜单。如果有几个同名的单选按钮,则最初被选中的(如果有)将匹配:default,即使 UI 已经被用户更新以不再匹配:checked。如果复选框在页面加载时被选中,:default将匹配它。在select元素中,任何最初选中的选项都将匹配:

[type="checkbox"]:default + label { font-style: italic; }
<input type="checkbox" id="chbx" checked name="foo" value="bar">
<label for="chbx">This was checked on page load</label>

:default 伪类还将匹配表单的默认按钮,通常是在给定表单中作为 DOM 顺序中第一个 button 元素的成员。这可用于向用户指示,如果他们只是按 Enter 键,哪个按钮将被激活,而不是显式选择要激活的按钮。

:placeholder-shown 伪类类似于它将选择具有标记级别定义的占位符文本的任何输入,只要该占位符文本可见。当输入具有值时,占位符将不再显示。例如:

<input type="text" id="firstName" placeholder="Your first name">
<input type="text" id="lastName"  placeholder="Your last name">

默认情况下,浏览器将 placeholder 属性的值放入输入字段中,通常比正常文本颜色浅。如果你想以一致的方式样式化这些输入元素,可以这样做:

input:placeholder-shown {opacity: 0.75;}

这将选择整个输入元素,而不是占位符文本本身。(要样式化占位符文本本身,请参阅“占位符文本伪元素”。)

:autofill 伪类与其他两者有些不同:它匹配任何由浏览器自动填充或自动完成值的元素。如果你曾经通过让浏览器填写存储的姓名、电子邮件、邮寄地址等来填写表单,这可能对你来说并不陌生。通常填充的输入字段会有一个明显的样式,如黄色背景。你可以使用 :autofill 来增强这种效果,比如这样:

input:autofill {border: thick solid maroon;}
注意

尽管你可以增加到自动填充文本的默认浏览器样式,但是覆盖浏览器内置的背景颜色等样式是困难的。这是因为浏览器为自动填充字段设置的样式几乎会覆盖任何其他设置,主要是为了为用户提供一致的自动填充内容体验和保护用户。

可选性伪类

:required 伪类匹配任何必填的用户输入元素,由 required 属性指示。:optional 伪类匹配没有 required 属性的用户输入元素,或其 required 属性的值为 false 的用户输入元素。

如果用户必须在提交表单之前为用户输入元素提供值,则用户输入元素为 :required。所有其他用户输入元素均匹配 :optional。例如:

input:required { border: 1px solid #f00;}
input:optional { border: 1px solid #ccc;}
<input type="email" placeholder="enter an email address" required>
<input type="email" placeholder="optional email address">
<input type="email" placeholder="optional email address" required="false">

第一个电子邮件输入框将匹配 :required 伪类,因为存在 required 属性。第二个输入框是可选的,因此将匹配 :optional 伪类。第三个输入框也是如此,它有一个 required 属性,但值为 false

非用户输入元素既不可以是必填的也不可以是可选的。在非用户输入元素上包含 required 属性不会导致可选性伪类匹配。

有效性伪类

:valid伪类指的是符合其所有数据有效性要求的用户输入。另一方面,:invalid伪类指的是未能满足其所有数据有效性要求的用户输入。

有效性伪类:valid:invalid仅适用于具备数据有效性要求能力的元素:<div>永远不会匹配任何一个选择器,但是<input>可以根据界面当前状态匹配任何一个。

在下面的示例中,当输入无效时,将一个图像放置在任何具有焦点的电子邮件输入框的背景中,当输入有效时,将使用另一张图像,如图 3-21 所示:

input[type="email"]:focus {
  background-position: 100% 50%;
  background-repeat: no-repeat;
}
input[type="email"]:focus:invalid {
  background-image: url(warning.jpg);
}
input[type="email"]:focus:valid {
  background-image: url(checkmark.jpg);
}
<input type="email">

css5 0321

图 3-21. 样式化有效和无效的 UI 元素

请记住,这些伪类状态可能不会如您所预期的那样起作用。例如,截至 2022 年末,任何不需要的空电子邮件输入都将匹配:valid。尽管空输入不是有效的电子邮件地址,但未填写电子邮件地址是对可选输入的有效响应。如果尝试填写格式错误的地址或随意文本,则会由于不是有效的电子邮件地址而匹配:invalid

范围伪类

范围伪类包括:in-range,指的是其值在 HTML 的minmax属性设定的最小值和最大值之间的用户输入,以及:out-of-range,指的是其值低于最小值或高于控件允许的最大值的用户输入。

例如,考虑一个接受 0 到 1,000 之间数字的数字输入:

input[type="number"]:focus {
  background-position: 100% 50%;
  background-repeat: no-repeat;
}
input[type="number"]:focus:out-of-range {
  background-image: url(warning.jpg);
}
input[type="number"]:focus:in-range {
  background-image: url(checkmark.jpg);
}
<input id="grams" type="number" min="0" max="1000" />

在这个例子中,从 0 到 1,000,包括这两个值,意味着input元素将匹配:in-range。任何超出此范围的值,无论是用户输入还是通过 DOM 分配的,都将导致input匹配:out-of-range

:in-range:out-of-range伪类仅适用于具有范围限制的元素。不具有范围限制的用户输入,如电话类型的链接,将不会匹配任何一个伪类。

HTML 还具有step属性。如果一个值因为不匹配step值而无效,但仍在minmax值之间或等于这些值,它将匹配:invalid,同时匹配:in-range。一个值可以在范围内,同时也是无效的。

因此,在以下情况下,输入的值将同时显示为红色和粗体,因为值23在范围内,但不能被 10 整除:

input[type="number"]:invalid {color: red;}
input[type="number"]:in-range {font-weight: bold;}
<input id="by-tens" type="number" min="0" max="1000" step="10" value="23" />

可变性伪类

可变性伪类包括:read-write,指的是用户可以编辑的用户输入;和:read-only,匹配不可编辑的用户输入,包括单选按钮和复选框。只有用户可以通过输入改变其值的元素才能匹配:read-write

例如,在 HTML 中,一个非禁用的、非只读的 input 元素是 :read-write,任何带有 contenteditable 属性的元素也是如此。其他所有元素都匹配 :read-only

默认情况下,以下规则中的任何一个都不会匹配,因为 <textarea> 元素是可读写的,而 <pre> 元素是只读的:

textarea:read-only {opacity: 0.75;}
pre:read-write:hover {border: 1px dashed green;}

然而,每个可以如下匹配:

<textarea disabled></textarea>
<pre contenteditable>Type your own code!</pre>

因为 <textarea> 被赋予了 disabled 属性,所以它变成了只读元素,因此将适用第一个规则。类似地,这里的 <pre> 通过 contenteditable 属性被设置为可编辑,所以现在它是一个可读写的元素。这将匹配第二个规则。

:lang():dir() 伪类

当你想根据元素的语言选择元素时,可以使用 :lang() 伪类。在匹配模式上,这个伪类类似于 |= 属性选择器(参见“基于部分属性值的选择”)。例如,要将使用法语书写内容的元素设置为斜体,你可以写如下任何一个:

*:lang(fr) {font-style: italic;}
*[lang|="fr"] {font-style: italic;}

伪类选择器和属性选择器之间的主要区别在于,语言信息可以从多个来源推断,其中一些来源于元素本身之外。对于属性选择器,元素必须具有要匹配的属性才能匹配。另一方面,:lang() 伪类匹配带有语言声明的元素的后代元素。正如选择器级别 3中所述:

在 HTML 中,语言是通过 lang 属性以及可能来自 meta 元素和协议(例如 HTTP 标头)的信息组合确定的。XML 使用一个名为 xml:lang 的属性,可能还有其他特定于文档语言的方法来确定语言。

伪类将在所有这些信息上操作,而属性选择器仅当元素的标记中存在 lang 属性时才能工作。因此,在大多数需要特定于语言的样式的情况下,伪类可能比属性选择器更为强大且可能是更好的选择。

CSS 还有一个 :dir() 伪类,它根据元素的 HTML 方向选择元素。例如,你可以选择所有方向为从右到左的元素,如下所示:

*:dir(rtl) {border-right: 2px solid;}

在这里需要注意的是,:dir() 伪类是基于 HTML 中元素的方向性进行选择的,而不是应用于它们的 CSS direction 属性的值。因此,你真正可以用于选择的仅有两个值是 ltr(从左到右)和 rtl(从右到左),因为这是 HTML 支持的唯一方向值。

逻辑伪类

除了结构和语言之外,一些伪类旨在为 CSS 选择器带来一丝逻辑和灵活性。

否定伪类

我们到目前为止讨论过的每个选择器都有一个共同点:它们都是正向选择器。它们用于识别应选择的内容,因此默认排除所有不匹配的内容。

有时您希望反转这个公式并根据元素的特性选择元素时,CSS 提供了否定伪类:not()。恰如其名,它与任何其他选择器都不太相似,并且在使用时有一些限制,但我们从一个例子开始。

假设您想要将样式应用于每个不具有classmoreinfo的列表项,如图 3-22 所示。过去这是非常困难的,在某些情况下甚至是不可能的。现在我们可以声明如下:

li:not(.moreinfo) {font-style: italic;}

css5 0322

图 3-22. 样式化没有特定类的列表项

:not()的工作方式是将其附加到选择器上,然后在括号内填入描述原始选择器无法匹配的选择器或一组选择器。

让我们反转之前的例子,并选择所有具有moreinfo类的元素,但不是列表项。这在图 3-23 中有所说明:

.moreinfo:not(li) {font-style: italic;}

css5 0323

图 3-23. 样式化具有特定类的元素,但不是列表项

从英语翻译过来,选择器将会说:“选择所有具有包含单词moreinfo的类值的元素,只要它们不是<li>元素。”类似地,li:not(.moreinfo)的翻译将是:“选择所有<li>元素,只要它们没有包含单词moreinfo的类值。”

您还可以在更复杂的选择器中的任何位置使用否定伪类。因此,要选择所有不是<section>元素子代的表格,您可以编写*:not(section) > table。同样地,要选择不属于表头的表头单元格,您可以编写类似table *:not(thead) > tr > th的内容,结果如图 3-24 所示。

css5 0324

图 3-24. 样式化表头单元格不在表头区域内的情况

您不能嵌套否定伪类;因此,p:not(:not(p))是无效的并且会被忽略。逻辑上讲,它等同于只写p,所以没有意义。此外,在括号内部不能引用伪元素(我们稍后将讨论)。

从技术上讲,可以将通用选择器放入括号中,但意义不大。毕竟,p:not(*)意味着“选择任何<p>元素,只要它不是任何元素”,而不存在不是元素的元素。类似地,p:not(p)也不会选择任何内容。也可以编写类似于p:not(div)的内容,这将选择任何不是<div>元素的<p>元素—换句话说,所有的<p>元素。再次说明,这样做的理由很少。

另一方面,可以链接否定来创建一种“并且也不是这个”的效果。例如,您可能想选择所有具有link类的元素,既不是列表项也不是段落:

*.link:not(li):not(p) {font-style: italic;}

这意味着“选择所有类值包含单词link的元素,只要它们既不是<li>元素也不是<p>元素。”这曾经是排除一组元素的唯一方法,但 CSS(和浏览器)支持否定选择器列表。这使我们可以像这样重新编写前面的例子:

*.link:not(li, p) {font-style: italic;}

与此同时,还可以使用更复杂的选择器,例如使用后代结合器。如果您需要选择所有从<form>元素继承但不是立即跟在<p>元素后面的元素,可以这样写:

form *:not(p + *)

翻译后,这意味着“选择不是相邻同级的<p>元素,且也是<form>元素后代的任何元素。”您可以将这些内容分组,所以如果您还想排除列表项和表头单元格,可以这样写:

form *:not(p + *, li, thead > tr > th)
警告

在早期的 2021 年,只有一些浏览器支持在:not()中使用复杂选择器,因此在使用时要特别谨慎,特别是在旧设置中。

使用:not()时需要注意的一件事是,在某些情况下,规则可能会以意想不到的方式组合,主要是因为我们不习惯于否定选择。考虑以下测试案例:

div:not(.one) p {font-weight: normal;}
div.one p {font-weight: bold;}
<div class="one">
   <div class="two">
      <p>I'm a paragraph!</p>
   </div>
</div>

段落将显示为粗体,而不是普通文本。这是因为两条规则都匹配:<p>元素是从一个类不包含单词one<div><div class="two">)继承的,但同时又是从一个类包含单词one<div>继承的。两条规则都匹配,因此都适用。由于存在冲突,级联(在第四章中解释)用于解决冲突,第二条规则胜出。标记的结构排列,如div.two比段落更“接近”,是无关紧要的。

:is():where()伪类

CSS 有两个伪类允许在复杂选择器中进行组匹配,即:is():where()。它们几乎完全相同,只有一个微小的差别,等您理解它们如何工作后我们再详细讨论。让我们先从:is()开始。

假设您想选择所有列表项,无论它们是否属于有序列表或无序列表。传统的方法如下所示:

ol li, ul li {font-style: italic;}

使用 :is(),我们可以这样重写:

:is(ol, ul) li {font-style: italic;}

匹配的元素将完全相同:所有作为有序或无序列表的一部分的列表项。

这似乎有点毫无意义:语法不仅稍微不那么清晰,而且还要多一个字符。事实上,在像这样简单的情况下,:is() 并不是特别引人注目。然而,情况越复杂,:is() 就越能发挥作用。

例如,如果我们想要样式化所有至少在嵌套列表中深入两级的列表项,无论在它们上面有什么组合的有序和无序列表?比较下面的规则,两者都会产生与图 3-25 相同的效果,只是一个使用了传统方法,另一个使用了 :is()

ol ol li, ol ul li, ul ol li, ul ul li {font-style: italic;}

:is(ol, ul) :is(ol, ul) li {font-style: italic;}

css5 0325

图 3-25. 使用 :is() 选择元素

现在考虑一下传统方法在三、四甚至更多层嵌套列表中的情况会是什么样子!

:is() 伪类可用于各种情况;选择位于标题、页脚和 <nav> 元素内部的列表中的所有链接可能看起来像这样:

:is(header,footer,nav,#header,#footer) :is(ol,ul) a[href] {font-style: italic;}

更好的是::is() 内部的选择器列表被称为宽容的选择器列表。默认情况下,如果选择器中的任何一部分无效,整个规则都将被标记为无效。而宽容的选择器列表则会丢弃任何无效的部分,并继续尊重其余部分。

所以,考虑到这一切,:is():where() 有什么区别呢?唯一的区别在于,:is() 在其选择器列表中采用最具体选择器的特异性,而 :where() 的特异性为零。如果你对最后一句话感到困惑,别担心!我们还没有讨论特异性,但将在下一章中进行讨论。

警告

:is():where() 仅在 2021 年初才进入浏览器,因此在使用它们时要格外小心,特别是在传统设置中。

选择已定义的元素

随着网络的进步,它增加了越来越多的功能。其中较新的一项是以标准化的方式向标记添加自定义 HTML 元素。这在模式库中经常发生,模式库通常根据特定于库的元素定义 Web 组件。

此类库为了更高效率而做的一件事是推迟定义元素直到需要它,或者准备好填充应该放入其中的任何内容。这样的自定义元素在标记中可能如下所示:

<mylib-combobox>options go here</mylib-combobox>

实际目标是填充组合框(一个允许用户输入任意值的下拉列表),使用后端 CMS 提供的任何选项,通过请求最新数据的脚本在本地构建列表,并在过程中移除占位文本。然而,如果服务器未能响应,导致自定义元素未定义并陷入占位文本的状态,会发生什么?如果不采取措施,文本“options go here”将被插入页面中,可能只有最少的样式。

这就是:defined派上用场的地方。你可以使用它来选择任何已定义的元素,并结合使用:not()来选择尚未定义的元素。下面是隐藏未定义组合框的简单方法,以及应用样式到已定义组合框的方法:

mylib-combobox:not(:defined) {display: none;}
mylib-combobox:defined {display: inline-block;}
mylib-combobox {font-size: inherit; border: 1px solid gray;
   outline: 1px solid silver;}

伪类:has()

:has()伪类有点棘手,因为它并不完全遵循我们到目前为止所遵循的所有规则——但正因如此,它也非常强大。

想象一下,你想对任何包含图片的<div>元素应用特殊样式。换句话说,如果一个<div>元素内部包含一个<img>元素,你想对<div>应用某些样式。而这正是:has()可以实现的。

前面的示例会类似这样编写,结果如图 3-26 所示:

div:has(img) {
	border: 3px double gray;
}
<div>
   <img src="masthead.jpg" alt="">
</div>
<div>
   <p>No image here!</p>
</div>
<div>
   <p>This has text and <img src="redbox.gif" alt="an image">.
</div>

css5 0326

图 3-26. 使用:has()选择元素

第二个<div>,其内部没有<img>元素作为子元素,因此不会显示边框。如果你只想让第一个<div>显示边框,因为你实际上只想为直接包含图片的<div>元素设置样式,只需修改选择器使用子元素组合器,像这样:div:has(> img)。这样可以防止第三个<div>显示边框。

伪类:has()在某种实际意义上是神话般的“父选择器”,CSS 作者从 CSS 诞生以来一直希望拥有这样的功能。但它不仅仅是用于父级选择,因为你可以基于兄弟元素进行选择,或者在祖先链中任意深度进行选择。如果以上内容还不太明白,别急:我们会进一步解释。

我们首先要注意两点:

  • :has()的括号内,你可以提供一个逗号分隔的选择器列表,每个选择器可以是简单、复合或复杂的。

  • 这些选择器相对于锚点元素考虑。

让我们按顺序来看。以下都是有效的:has()用法:

table:has(tbody th) {…}
/* tables whose body contains table headers */

a:any-link:has(img:only-child) {…}
/* links containing only an image */

header:has(nav, form.search) {…}
/* headers containing either nav or a form classed search */

section:has(+ h2 em, table + table, ol ul ol ol) {…}
/* sections immediately followed by an 'h2' that contains an 'em'
 OR that contain a table immediately followed by another table
 OR that contain an 'ol' inside an 'ol' inside a 'ul' inside an 'ol' */

或许上面的例子有点令人不知所措,让我们再详细解释一下。我们可以用更详细的方式重述,如下:

section:has(+ h2 em),
section:has(table + table),
section:has(ol ul ol ol) {…}

这里有两个将被选中的标记模式示例:

<section>(…section content…)</section>
<h2>I’m an h2 with an <em>emphasis element</em> inside, which means
    the section right before me gets selected!</h2>

<section>
<h2>This h2 doesn’t get the section selected, because it’s a child of
    the section, not its immediately following sibling</h2>
<p>This paragraph is just here.</p>
<aside>
<h3>Q1 Results</h3>
<table>(…table contents…)</table>
<table>(…table contents…)</table>
</aside>
<p>Those adjacent-sibling tables mean this paragraph’s parent section element
   DOES get selected!</p>
</section>

在第一个示例中,选择不是基于父级或任何其他祖先关系;相反,选择了<section>,因为它的直接同级(<h2>)有一个<em>元素作为其后代之一。在第二个示例中,选择了<section>,因为它有一个后代<table>,紧接着另一个<table>,这两个<table>在这种情况下都在一个<aside>元素内。这使得这个特定的例子成为了祖父选择,而不是父选择,因为<section>是表格的祖父。

对,这是我们之前提到的第一个要点。第二个是括号内的选择器是相对于带有:has()的元素。这意味着,例如,以下选择器永远不会匹配任何内容:

div:has(html body h1)

这是因为虽然<h1>肯定可以是<div>的后代,但<html><body>元素不能。这个选择器的意思,翻译成英语,是“选择任何具有后代<html><div>,它本身具有后代<body>,后者具有后代<h1>。”<html>元素永远不会是<div>的后代,因此这个选择器无法匹配。

为了选择一个更现实的例子,这里有一些标记,显示了嵌套的列表,该文档结构在图 3-27 中显示:

<ol>
<li>List item</li>
<li>List item
	<ul>
	<li>List item</li>
	<li>List item</li>
	<li>List item</li>
	</ul>
</li>
<li>List item</li>
<li>List item</li>
<li>List item
	<ul>
	<li>List item</li>
	<li>List item
		<ol>
		<li>List item</li>
		<li>List item</li>
		<li>List item</li>
		</ol>
	</li>
	<li>List item</li>
	</ul>
</li>
</ol>

css5 0327

图 3-27。文档结构的片段

对于这个结构,我们将应用以下规则。剧透警告:其中一个将匹配一个元素,而另一个则不会:

ul:has(li ol) {border: 1px solid red;}
ul:has(ol ul ol) {font-style: italic;}

第一条导致浏览器查看所有<ul>元素。对于它找到的任何<ul>,它会查看从该<ul>下降的元素的结构。如果在后代元素中找到了li ol关系,则匹配该<ul>,在这种情况下会给它一个红色边框。

如果我们研究标记结构,无论是在代码中还是在图 3-27 中,我们可以看到两个<ul>元素。第一个有<li>后代但没有任何<ol>后代,因此不会被匹配。第二个<ul>也有<li>后代,其中一个有一个<ol>后代。它是匹配的!这个<ul>将被给予一个红色边框。

第二条规则还会导致浏览器查看所有<ul>元素。在这种情况下,对于它找到的任何<ul>,浏览器会查找其中的ol ul ol关系,这些关系是在<ul>的后代元素中进行的。不计算<ul>外的元素:只有其中的元素会被考虑。在文档中的两个<ul>元素中,都没有一个<ul>内部有一个<ol>,后者又在另一个<ol>内部,并且自身是从被考虑的<ul>下降的。没有匹配,因此两个<ul>元素都不会被斜体化。

更强大的是,你可以自由地将 :has() 与其他伪类混合使用。例如,如果希望选择任何包含图像的标题级别,可以通过两种方式实现:冗长而笨拙的方式或紧凑的方式。这两种方式都在这里展示了:

h1:has(img), h2:has(img), h3:has(img), h4:has(img), h5:has(img), h6:has(img)

:is(h1, h2, h3, h4, h5, h6):has(img)

这两个选择器具有相同的结果:如果一个元素 所列的标题元素之一,并且该元素的后代元素中包含 <img> 元素,则将选择该标题。

同样,你也可以选择任何 包含图像的标题:

:is(h1, h2, h3, h4, h5, h6):not(:has(img))

在这里,如果一个元素 所列的标题级别之一,但 <img> 元素 是其后代之一,则将选择该标题。如果我们将它们组合在一起,并应用于多个标题,我们将得到 图 3-28 所示的结果。

css5 0328

图 3-28. To has and has not

正如你已经看到的,这个选择器具有很强的功能。也存在一些危险:完全可能编写导致浏览器性能严重受损的选择器,特别是在使用脚本修改文档结构的环境中。考虑以下情况:

div:has(*.popup) {…}

这是说:“将这些样式应用于任何具有 popup 类作为后代元素的 <div>。” 当页面加载到浏览器中时,它必须检查所有 <div> 元素,以查看它们是否匹配此选择器。这可能意味着在文档的结构树上下几次访问,但理想情况下应在几毫秒内解决,并且页面可以显示。

但假设我们有一个脚本,可以在页面上的一个元素或甚至几个元素上添加 .popup。一旦类值更改,浏览器不仅必须检查是否有任何样式适用于 .popup 元素及其后代,还必须检查任何祖先或同级元素是否受此更改的影响。浏览器现在不仅查看文档树下部,还必须向上查找。这种触发的任何更改都可能意味着整个页面布局的变化,无论何时元素标记为 .popup,或者当 .popup 元素失去该类值时,都可能影响文档完全不同部分的元素。

这种性能影响是为什么以前没有“父选择器”或类似物。计算机速度足够快,浏览器引擎足够智能,这比过去少得多,但仍需注意并彻底测试。

注意

has() 中不可能嵌套伪元素如 ::first-line::selection。(我们稍后会讨论伪元素。)

其他伪类

CSS 选择器规范中定义了更多伪类,但它们在浏览器中仅部分支持,或在某些情况下根本不支持,截至 2023 年初,或者是我们将在书中其他地方讨论的内容。我们将它们列在表 3-5 中,以保证完整性,并指向可能在本书版本和下一版之间支持的伪类。 (或者可以用具有不同名称的等效伪类替换;有时会发生这种情况。)

表 3-5. 其他伪类

名称 描述
:nth-col() 指的是处于第 n 列的表格单元格或网格项,这是使用an + b模式找到的;基本上与:nth-child()相同,但专门用于表格或网格列
:nth-last-col() 指的是处于第 n 个末列的表格单元格或网格项,这是使用an + b模式找到的;基本上与:nth-last-child()相同,但专门用于表格或网格列
:left 指的是打印文档中的任何左手页面;更多信息请参见第二十一章
:right 指的是打印文档中的任何右手页面;更多信息请参见第二十一章
:fullscreen 指的是全屏显示的元素(例如全屏模式下的视频)
:past 指的是出现在匹配:current的元素之前(按时间顺序)的元素
:current 指的是当前以时间为基础显示的元素或元素的祖先(例如,包含闭幕字幕文本的元素)
:future 指的是出现在匹配:current的元素之后(按时间顺序)的元素
:paused 指的是处于“播放”状态或“暂停”状态的任何元素(例如音频、视频等)当处于“暂停”状态时
:playing 指的是处于“播放”状态或“暂停”状态的任何元素(例如音频、视频等)
:picture-in-picture 指的是用作画中画显示的元素

伪元素选择器

就像伪类为锚点分配了虚拟类一样,伪元素插入虚构的元素到文档中以达到某些效果。

与伪类的单冒号不同,伪元素采用双冒号语法,例如::first-line。这是为了区分伪元素和伪类。在 CSS2 中,并非总是如此——两种选择器类型都使用单冒号,因此为了向后兼容,浏览器可能会接受某些单冒号伪类型选择器。不过,这并不是马虎的借口!始终使用正确数量的冒号来未雨绸缪你的 CSS;毕竟,没有办法预测浏览器何时会停止接受单冒号伪类型选择器。

样式化首字母

::first-letter伪元素样式化任何非内联元素的第一个字母或前导标点字符和第一个字母(如果文本以标点符号开头)。此规则导致每个段落的第一个字母都变成红色:

p::first-letter {color: red;}

::first-letter伪元素最常用于创建大写字母开头或降字母效果的排版效果。您可以使每个<p>的第一个字母比其余文字大两倍,尽管您可能希望仅将此样式应用于第一个段落的第一个字母:

p:first-of-type::first-letter {font-size: 200%;}

图 3-29 展示了这条规则的结果。

css5 0329

图 3-29. ::first-letter伪元素的效果

这条规则有效地导致用户代理样式化一个虚构或伪造的元素,该元素围绕每个<p>的第一个字母。它看起来可能像这样:

<p><p-first-letter>T</p-first-letter>his is a p element, with a styled first
    letter</h2>

::first-letter样式仅应用于示例中显示的虚构元素的内容。这个<p-first-letter>元素不会出现在文档源中,甚至不会出现在 DOM 树中。相反,用户代理会即时构建它的存在,并用来将::first-letter样式应用于适当的文本部分。换句话说,<p-first-letter>是一个伪元素。请记住,您不需要添加任何新标签。用户代理会为您样式化第一个字母,就好像您将其置于一个样式化元素中一样。

第一个字母被定义为源元素的第一个排版字母单元,如果它没有前置其他内容,比如一个图片。规范使用术语字母单元,因为一些语言的字母由多个字符组成,例如古西诺尔斯语中的œ。即使有多个这样的符号,位于第一个字母单元之前或之后的标点也应包括在::first-letter伪元素中。浏览器会为您完成这一切。

样式化第一行

同样,::first-line可以用来影响元素中文本的第一行。例如,您可以使文档中每个段落的第一行变得大号和紫色:

p::first-line {
  font-size: 150%;
  color: purple;
}

在图 3-30 中,该样式应用于每个段落中显示的第一行文本。这在显示区域的宽度如何,无论多宽或多窄都成立。如果第一行仅包含段落的前五个单词,只有这五个单词会变得大号和紫色。如果第一行包含元素的前 30 个单词,所有 30 个单词都会变得大号和紫色。

css5 0330

图 3-30. ::first-line伪元素的效果

因为从“这”到“仅”之间的文本应该是大号和紫色,用户代理使用一个虚构的标记,看起来可能像这样:

<p>
<p-first-line>This is a paragraph of text that has only</p-first-line>
one stylesheet applied to it. That style causes the first line to
be big and purple. No other line will have those styles applied.
</p>

如果文本的第一行编辑后只包括段落的前七个字,那么虚构的</p-first-line>将移回并出现在“that”一词之后。如果用户增加或减少字体大小,或展开或收缩浏览器窗口导致文本宽度变化,从而导致第一行的字数增加或减少,浏览器将自动设置当前显示的第一行中的词语为大号且紫色。

第一行的长度取决于多个因素,包括字体大小、字母间距和父容器的宽度。根据标记和第一行的长度,第一行的结束可能在嵌套元素的中间。如果::first-line打断了一个嵌套元素,例如em或超链接,那么附加到::first-line的属性仅适用于该嵌套元素的显示在第一行的部分。

::first-letter::first-line的限制

::first-letter::first-line伪元素目前只能应用于块级元素,如标题或段落,而不能应用于内联元素,如超链接。对于::first-line::first-letter可以应用的 CSS 属性也有限制。表 3-6 提供了这些限制的概述。与所有伪元素一样,它们都不能包含在:has():not()中。

表 3-6. 伪元素上允许的属性

::first-letter ::first-line

|

  • 所有字体属性

  • 所有背景属性

  • 所有文本装饰属性

  • 所有内联排版属性

  • 所有内联布局属性

  • 所有边框属性

  • box-shadow

  • color

  • opacity

|

  • 所有字体属性

  • 所有背景属性

  • 所有边距属性

  • 所有填充属性

  • 所有边框属性

  • 所有文本装饰属性

  • 所有内联排版属性

  • color

  • opacity

|

占位符文本伪元素

正好,通过::first-line可以应用的样式的限制与通过::placeholder应用的样式的限制完全相同。这个伪元素匹配任何放置在文本输入框和文本区域中的占位符文本。例如,你可以将文本输入框的占位符文本设置为斜体,将文本区域的占位符文本设置为浅蓝色,就像这样:

input::placeholder {font-style: italic;}
textarea::placeholder {color: cornflowerblue;}

对于<input><textarea>元素,这段文本由 HTML 中的placeholder属性定义。标记看起来可能非常像这样:

<input type="text" placeholder="(XXX) XXX-XXXX" id="phoneno">
<textarea placeholder="Tell us what you think!"></textarea>

如果在<input>元素的value属性或<textarea>元素内放置内容来预填文本,这将覆盖任何placeholder属性的值,并且结果文本不会被::placeholder伪元素选中。

表单按钮伪元素

谈到表单元素,也可以直接选择文件选择按钮——仅限文件选择按钮——在 typefile<input> 元素中。这样可以突出显示用户需要单击以打开文件选择对话框的按钮,即使输入的其他部分无法直接样式化。

如果你从未见过文件选择输入框,它通常是这样的:

<label for="uploadField">Select file from computer</label>
<input id="uploadField" type="file">

那第二行会被一个控件替换,其外观取决于操作系统和浏览器的组合,因此它在不同用户之间看起来至少有点不同(有时差异很大)。图 3-31 展示了输入框的一个可能渲染,按钮由以下 CSS 样式化:

input::file-selector-button {
   border: thick solid gray;
   border-radius: 2em;
}

css5 0331

图 3-31. 样式化文件提交输入框中的按钮

生成内容的前后元素

假设你想在每个 <h2> 元素之前加上一对银色方括号作为排版效果:

h2::before {content: "]]"; color: silver;}

CSS 允许你插入生成的内容,然后直接使用伪元素 ::before::after 进行样式化。图 3-32 提供了一个示例。

css5 0332

图 3-32. 在元素前插入内容

伪元素用于插入生成的内容并对其进行样式化。要在元素末尾、在关闭标签之前放置内容,请使用伪元素 ::after。你可以结束你的文档并适当地完成:

body::after {content: "The End.";}

另外,如果你想在元素开头插入一些内容,在开标签之后使用 ::before。只需记住,在任一情况下,都必须使用 content 属性来插入并样式化内容。

生成的内容是自己的主题,整个主题(包括更多关于 ::before::aftercontent 的详细信息)在 第 16 章 中更全面地讨论。

高亮伪元素

CSS 中的一个相对较新的概念是能够样式化已经被高亮的内容片段,无论是通过用户选择还是用户代理本身。这些在 表 3-7 中总结。

表 3-7. 高亮伪元素

Name Description
::selection 指任何已被高亮以供用户操作的文档部分(例如,用鼠标拖选的文本)
::target-text 指文档中已被定位的文本;这与 :target 伪类不同,后者指的是作为整体被定位的元素,而不是文本片段。
::spelling-error 指用户代理已标记为拼写错误的文档部分
::grammar-error 指用户代理已标记为语法错误的文档部分

在 表 3-7 中的四个伪元素中,只有一个 ::selection 在 2023 年初有明显的支持。因此,我们将探索它,并将其余的留给未来的版本。

当有人使用鼠标指针点击、按住并拖动以突出显示某些文本时,这就是一个选择操作。大多数浏览器为文本选择设置了默认样式。作者可以对这些选择应用一组有限的 CSS 属性,通过样式化 ::selection 伪元素来覆盖浏览器的默认样式。假设您希望选择的文本是白色的,背景是海军蓝。CSS 将如下所示:

::selection {color: white; background-color: navy;}

::selection 的主要用例是指定选定文本的颜色方案,使其与设计的其余部分不冲突,或为文档的不同部分定义不同的选择样式。例如:

::selection {color: white; background-color: navy;}
form::selection {color: silver; background-color: maroon;}

在样式化选择高亮时要小心:用户通常希望他们选择的文本看起来某种方式,通常由其操作系统中的设置定义。因此,如果您在选择样式上过于聪明,可能会使用户感到困惑。尽管如此,如果您知道选择的文本由于设计的颜色倾向于模糊而难以看到,定义更明显的高亮样式可能是一个好主意。

请注意,所选文本可以跨越元素边界,并且在给定文档中可以存在多个选择。假设用户从一个段落中间开始选择文本,直到下一个段落中间。实际上,每个段落都将有其自己的选择伪元素嵌套在内部,并且选择的样式将根据上下文进行处理。考虑到以下的 CSS 和 HTML,您将获得类似于 图 3-33 所示的结果:

.p1::selection {color: silver; background-color: black;}
.p2::selection {color: black; background-color: silver;}
<p class="p1">This is a paragraph with some text that can be selected,
   one of two.</p>
<p class="p2">This is a paragraph with some text that can be selected,
   two of two.</p>

css5 0333

图 3-33. 选择样式

这再次强调了前面提到的一点:在选择样式上要小心。如果您的选择样式与用户的默认选择样式互动不良,可能会使某些用户的文本变得难以阅读。

此外,基于用户隐私原因,您只能将一些有限的 CSS 属性应用于选择:colorbackground-colortext-decoration及其相关属性、text-shadowstroke 属性(在 SVG 中)。

注意

自 2023 年初起,选择不会继承其样式:选择包含某些内联元素的文本将使选择样式应用于内联元素之外的文本,但不会应用于内联元素内的文本。目前尚不清楚这种行为是否有意为之,但它在主要浏览器中是一致的。

除了 ::selection 外,可能还会逐渐支持 ::target-text。截至 2023 年初,这仅在 Chromium 浏览器中受支持,它引入了一项需要的功能。使用此功能,文本可以作为 URL 的片段标识符的一部分添加到页面的末尾,以突出显示一个或多个部分。

例如,URL 可能看起来像这样:https://example.org/#:~:text=for%20use%20in%20illustrative%20examples。末尾的部分告诉浏览器:“一旦加载页面,突出显示这些文本的任何示例。”这些文本被编码用于 URL,这就是为什么它填充了%20字符串——它们表示空格。结果看起来可能类似于图 3-34。

css5 0334

图 3-34. 目标文本样式

如果你想在自己的页面上抑制此内容的高亮显示,你可以做如下操作:

::target-text {color: inherit; background-color: inherit;}

至于::spelling-error::grammar-error,它们的作用是对文档中的拼写或语法错误进行某种形式的高亮显示。你可以看到像 Google Docs 或 WordPress 或 Craft 等内容管理系统的编辑字段中有这样的实用性。不过,在其他大多数应用程序中,它们似乎不太可能非常受欢迎。无论如何,在撰写本文时,这两者都没有得到浏览器的支持,工作组仍在讨论它们应该如何工作的细节。

背景伪元素

假设你有一个全屏显示的元素,比如一个视频。此外,假设该元素没有完全填充到屏幕边缘,也许是因为元素的宽高比与屏幕的宽高比不匹配。那么,对于元素没有覆盖到的屏幕部分,应该填充什么?你如何使用 CSS 选择这些非元素区域?

进入::backdrop伪元素。它表示一个与全屏视口完全相同大小的盒子,并且始终绘制在全屏元素的下方。因此,你可以像这样在任何全屏视频的背后放置一个深灰色的背景:

video::backdrop {background: #111;}

CSS 并不限制可以应用于背景的样式,但由于它们基本上是放置在全屏元素后面的空箱子,所以大多数情况下,你可能只会设置背景颜色或图像。

重要的一点是背景不参与继承。它们不能从祖先元素继承样式,也不会将它们的任何样式传递给任何子元素。你应用于背景的任何样式都将存在于它们自己的小宇宙中。

视频提示伪元素

谈到视频,视频通常具有包含文本字幕的 Web 视频文本轨道(WebVTT)数据,从而实现辅助功能。这些字幕称为提示,并可以使用::cue伪元素进行样式设置。

假设你有一个大部分是黑暗的视频,但有几个亮色片段。你可以将提示样式设置为浅灰色文字,放在半透明的黑色背景上,如下所示:

::cue {
  color: silver;
  background: rgba(0,0,0,0.5);
}

这将始终应用于当前可见的提示。

您还可以通过在括号内使用选择器模式来选择个别提示的部分。这可以用于样式化 WebVTT 数据中允许的一个小列表中定义的特定元素。例如,可以如下选择任何斜体提示文本:

::cue(i) {…}

您可以使用结构伪类如 :nth-child,但这些仅适用于给定提示内的元素,而不是跨提示。您不能选择每个其他提示来进行样式化,但可以选择给定提示内的每个其他元素。假设以下是 WebVTT 数据:

00:00:01.500 --> 00:00:02.999
<v Hildy>Tell me, is the lord of the universe in?</v>

00:00:03.000 --> 00:00:04.299
- Yes, he's in.
- In a bad humor.

第二个提示包括两行文本。实际上,这些被视为单独的元素,即使没有指定元素。因此,我们可以将希尔迪说的那行(用 <v Hildy> 指示,这是 <v voice="Hildy"> 的 WebVTT 等效项)设置为粗体,并且给第二个提示中的两行对话设置不同的颜色,如下所示:

::cue(v[voice="Hildy"]) {font-weight: bold;}
::cue(:nth-child(odd)) {color: yellow;}
::cue(:nth-child(even)) {color: white;}

截至 2023 年初,可以应用于提示的属性范围有限:

  • color

  • background 及其相关的长手属性(例如,background-color

  • text-decoration 及其相关的长手属性(例如,text-decoration-thickness

  • text-shadow

  • text-combine-upright

  • font 及其相关的长手属性(例如,font-weight

  • ruby-position

  • opacity

  • visibility

  • white-space

  • outline 及其相关的长手属性(例如,outline-width

影子伪类和-元素

HTML 中的另一个最新创新是引入了影子 DOM,这是一个深入而复杂的主题,我们在这里没有空间来探讨。在基本水平上,影子 DOM 允许开发人员在常规(或)DOM 内创建封装的标记、样式和脚本。这使得一个影子 DOM 的样式和脚本不会影响文档中的其他任何部分,无论这些部分是在轻或影子 DOM 中。

我们在这里提到这一点是因为 CSS 确实提供了一些方法来连接到影子 DOM,以及从影子 DOM 内部到达并选择托管影子的轻 DOM 的方式。(这一切听起来很面板艺术,不是吗?)

影子伪类

要了解这意味着什么,让我们回顾一下本章早些时候的组合框示例。它看起来是这样的:

<mylib-combobox>options go here</mylib-combobox>

所有在此自定义元素内的 CSS(和 JS)仅适用于 <mylib-combobox> 元素。即使自定义元素内的 CSS 声明了像 li {color: red;} 这样的样式,那也只会应用于 <mylib-combobox> 内构建的 <li> 元素。它不会泄漏到页面上其他地方的列表项来使它们变成红色。

这一切都很好,但是如果你想从自定义元素内部以某种方式样式化宿主元素,该怎么办?宿主元素,在这种情况下通常被称为影子宿主,即<mylib-combobox>。从影子宿主内部,CSS 可以使用:host伪类选择宿主。例如:

:host {border: 2px solid red;}

那会“穿透影子边界”(用规范中的生动词语来说),并选择 <mylib-combobox> 元素,或者无论影子 DOM CSS 名称是什么的自定义元素。

现在,假设可以有不同类型的组合框,每个都有自己的类。类似这样的情况:

<mylib-combobox class="countries">options go here</mylib-combobox>
<mylib-combobox class="regions">options go here</mylib-combobox>
<mylib-combobox class="cities">options go here</mylib-combobox>

您可能希望将每类组合框样式化不同。为此,存在:host()伪类:

:host(.countries) {border: 2px solid red;}
:host(.regions) {border: 1px solid silver;}
:host(.cities) {border: none; background: gray;}

这些规则随后可以包含在所有组合框加载的样式中,使用影子宿主上的类来适当地进行样式设置。

但等等!如果我们不想依赖类,而是想根据它们在光 DOM 中出现的位置样式化我们的影子宿主怎么办?在这种情况下,:host-context()就能帮到您。因此,如果它们是表单的一部分,则可以以一种方式样式化我们的组合框,如果它们是标题导航元素的一部分,则以另一种方式样式化:

:host-context(form) {border: 2px solid red;}
:host-context(header nav) {border: 1px solid silver;}

其中的第一个意味着“如果影子宿主是 <form> 元素的后代,则应用这些样式”。第二个意味着“如果影子宿主是 <nav> 元素的后代,并且 <nav> 元素本身是 <header> 元素的后代,则应用这些样式。”要明确的是,在这些情况下,form<nav> 不是 影子宿主!:host-context()中的选择器仅描述了选择宿主所需放置的上下文。

当在影子 DOM 的上下文中声明时,这四个穿越影子 DOM/光 DOM 边界的选择器——:host:host():host-content(),以及接下来讨论的:slotted()选择器——仅在支持影子 DOM 的情况下生效。截至 2023 年初,Safari 或 Firefox 不支持:host-context(),且有可能从规范中删除。

影子伪元素

除了拥有宿主外,影子 DOM 还可以定义插槽。这些是要将其他东西插入其中的元素,就像您将扩展卡插入扩展槽一样。让我们稍微扩展一下组合框的标记:

<mylib-combobox>
     <span slot="label">Country</span>
     ["shadow-tree"]
          <slot name="label"></slot>
     [/"shadow tree"]
</mylib-combobox>

现在,要明确一点,shadow tree并不是实际的标记。它只是用来表示由构建它的脚本创建的影子 DOM。因此,请不要在您的文档中写入方括号引用的元素名称;它们会失败。

话虽如此,考虑到前述设置,<span>将插入到slot元素中,因为名称匹配。您可以尝试对插槽应用样式,但如果您更愿意对插入插槽的内容进行样式化呢?这由::slotted()伪元素表示,根据需要接受选择器。

因此,如果您想以一种方式样式化所有插槽元素,然后在插槽元素是 <span> 的情况下添加一些额外的样式,您可以这样写:

::slotted(*) {outline: 2px solid red;}
::slotted(span) {font-style-italic;}

更实际地说,您可以将所有插槽样式设置为红色,然后从任何已插槽内容的插槽中删除该红色,从而使未获取任何内容的插槽显眼。类似这样的做法:

slot {color: red;}
::slotted(*) {color: black;}

影子 DOM 及其使用是一个复杂的主题,而我们在本节中甚至还没有开始深入探讨它。我们唯一的目标是介绍与影子 DOM 相关的伪类和伪元素,而不是解释影子 DOM 或阐明最佳实践。

总结

正如你在本章中看到的,伪类和伪元素为样式表提供了强大和灵活的功能。无论是根据超链接的访问状态选择元素,根据文档结构中的位置匹配元素,还是样式化影子 DOM 的部分,几乎每种口味都有相应的伪选择器。

在本章和前一章中,我们已经多次提到了特异性和级联的概念,并承诺很快会详细讨论它们。现在就是时候了。这正是我们将在下一章中做的事情。

第四章:具体性、继承和级联

第二章和第三章展示了文档结构和 CSS 选择器如何允许你对元素应用各种样式。知道每个有效文档生成一个结构树,你可以创建基于祖先元素、属性、兄弟元素等进行选择的选择器。结构树是选择器正常工作的基础,也是 CSS 中继承同样重要的一个方面。

继承 是某些属性值从一个元素传递到后代元素的机制。在确定应将哪些值应用于元素时,用户代理必须考虑不仅继承,还要考虑声明的具体性以及声明本身的来源。这个考虑的过程就是所谓的级联

我们将在本章探讨这三种机制——具体性、继承和级联——之间的相互关系。目前,后两者的区别可以总结如下:当我们编写 h1 {color: red; color: blue;} 时,<h1> 变为蓝色是因为级联的影响,而 <h1> 内的任何 <span> 也因为继承而变为蓝色。

无论事物看起来多么抽象,都要坚持下去!你的坚持将会得到回报。

具体性

你从第二章和第三章知道,可以通过多种方式选择元素。事实上,同一个元素通常可以被两个或更多规则选中,每个规则都有自己的选择器。让我们考虑以下三对规则。假设每一对将匹配同一个元素:

h1 {color: red;}
body h1 {color: green;}

h2.grape {color: purple;}
h2 {color: silver;}

html > body table tr[id="totals"] td ul > li {color: maroon;}
li#answer {color: navy;}

每一对规则中只有一条可以应用或“胜出”,因为匹配的元素一次只能是一种颜色。我们怎么知道哪一个会胜出呢?

答案在每个选择器的具体性中找到。对于每个规则,用户代理(即 web 浏览器)评估选择器的具体性,并将具体性附加到具有优先级的级联层中的每个声明中。当一个元素有两个或更多冲突的属性声明时,具有最高具体性的声明将获胜。

注意

冲突解决的整个过程不仅仅是一个段落可以涵盖的那么简单。目前,只需记住,选择器的具体性仅与共享相同来源和级联层的其他选择器进行比较。我们将在“级联”一节中详细讨论这些术语及更多内容。

选择器的具体性由选择器本身的组成部分决定。具体性值可以用三个部分表示,如下所示:0,0,0。选择器的实际具体性如下确定:

  • 对于选择器中给定的每个 ID 属性值,添加 1,0,0

  • 对于选择器中给定的每个类属性值、属性选择或伪类,添加0,1,0

  • 对于选择器中给定的每个元素和伪元素,添加0,0,1

  • 组合符不对特异性做出任何贡献。

  • 任何列在:where()伪类内部和通用选择器中的内容,添加0,0,0。(虽然它们不对特异性权重做出贡献,但与组合符不同,它们确实匹配元素。)

  • :is():not():has()伪类的特异性等于其选择器列表参数中最具体选择器的特异性。

例如,以下规则的选择器得出指定的特异性:

h1 {color: red;}                     /* specificity = 0,0,1 */
p em {color: purple;}                /* specificity = 0,0,2 */
.grape {color: purple;}              /* specificity = 0,1,0 */
*.bright {color: yellow;}            /* specificity = 0,1,0 */
p.bright em.dark {color: maroon;}    /* specificity = 0,2,2 */
#id216 {color: blue;}                /* specificity = 1,0,0 */
*:is(aside#warn, code) {color: red;} /* specificity = 1,0,1 */
div#sidebar *[href] {color: silver;} /* specificity = 1,1,1 */

如果此例中的<em>元素同时符合第二条和第五条规则,那么该元素将是栗色,因为第六条规则的特异性大于第二条规则。

特别注意倒数第二个选择器*:is(aside#warn, code):is()伪类是一组特异性等于选择器列表中最具体选择器的少数伪类之一。在这里,选择器列表为aside#warn, codeaside#warn复合选择器的特异性为1,0,1code选择器的特异性为0,0,1。因此,整个:is()选择器的特异性设置为aside#warn选择器的特异性。

现在,让我们回到本节前面的规则对,并填写特异性:

h1 {color: red;}         /* 0,0,1 */
body h1 {color: green;}  /* 0,0,2 (winner)*/

h2.grape {color: purple;}  /* 0,1,1 (winner) */
h2 {color: silver;}        /* 0,0,1 */

html > body table tr[id="totals"] td ul > li {color: maroon;}  /* 0,1,7 */
li#answer {color: navy;}                                       /* 1,0,1
 (winner) */

我们在每一对中指示了胜出的规则;在每种情况下,这是因为特异性更高。注意它们的列出方式以及规则的顺序在这里并不重要。

在第二对中,选择器h2.grape胜出,因为它有一个额外的类:0,1,1击败了0,0,1。在第三对中,第二条规则胜出,因为1,0,1优于0,1,7。事实上,特异性值0,1,0将优于值0,0,13

这是因为值是从左到右比较的。特异性为1,0,0的规则将优先于任何以0开头的特异性,无论后面的数字是什么。因此,1,0,10,1,7更优,因为第一个值的第一个位置上的1胜过第二个值的第一个位置上的0

声明和特异性

一旦确定了选择器的特异性,特异性值将被赋予其所有关联的声明。考虑这条规则:

h1 {color: silver; background: black;}

对于特异性目的,用户代理必须将规则视为未分组为单独的规则。因此,前面的例子将变成以下形式:

h1 {color: silver;}
h1 {background: black;}

两者的特异性均为0,0,1,每个声明都赋予了这个值。分组选择器也会发生同样的拆分过程。给定规则,

h1, h2.section {color: silver; background: black;}

用户代理将其视为以下形式:

h1 {color: silver;}             /* 0,0,1 */
h1 {background: black;}         /* 0,0,1 */
h2.section {color: silver;}     /* 0,1,1 */
h2.section {background: black;} /* 0,1,1 */

当多个规则匹配同一元素且一些声明发生冲突时,这一点变得很重要。例如,考虑以下规则:

h1 + p {color: black; font-style: italic;}              /* 0,0,2 */
p {color: gray; background: white; font-style: normal;} /* 0,0,1 */
*.callout {color: black; background: silver;}           /* 0,1,0 */

当应用于以下标记时,内容将如 图 4-1 所示呈现:

<h1>Greetings!</h1>
<p class="callout">
It's a fine way to start a day, don't you think?
</p>
<p>
There are many ways to greet a person, but the words are not as important
as the act of greeting itself.
</p>
<h1>Salutations!</h1>
<p>
There is nothing finer than a hearty welcome from one's neighbor.
</p>
<p class="callout">
Although a steaming pot of fresh-made jambalaya runs a close second.
</p>

css5 0401

图 4-1. 不同规则如何影响文档

在每种情况下,用户代理确定哪些规则匹配给定元素,计算所有相关声明及其特异性,确定哪些规则胜出,然后将胜出的规则应用于元素以获取样式化结果。这些操作必须对每个元素、选择器和声明都执行。幸运的是,用户代理会自动且几乎即时地完成所有这些操作。这种行为是级联的重要组成部分,我们稍后在本章中将讨论。

解决多个匹配项

当一个元素被一组选择器中的多个选择器匹配时,将使用最特异的选择器。考虑以下 CSS:

li,            /* 0,0,1 */
.quirky,       /* 0,1,0 */
#friendly,     /* 1,0,0 */
li.happy.happy.happy#friendly { /* 1,3,1 */
   color: blue;
}

这里我们有一个带有组合选择器的规则,每个单独的选择器具有非常不同的特异性。现在假设我们在 HTML 中找到了这个:

<li class="happy quirky" id="friendly">This will be blue.</li>

组合选择器中的每一个选择器都应用于列表项!哪一个用于特异性目的?最特异的那个。因此,在这个例子中,蓝色将以1,3,1的特异性应用。

你可能已经注意到,在某个选择器中我们重复了 happy 类名三次。这是一种技巧,可以用于类、属性、伪类,甚至是 ID 选择器,以增加特异性。使用时需小心,因为人为提高特异性可能会在将来造成问题:你可能希望用另一条规则覆盖该规则,而那条规则需要更多链式类名。

选择器特异性归零

通用选择器不会影响特异性。它具有0,0,0的特异性,这与没有特异性(正如我们将在 “继承” 中讨论的)不同。因此,根据以下两条规则,从 <div> 继承的段落将是黑色,但所有其他元素将是灰色:

div p {color: black;} /* 0,0,2 */
* {color: gray;}      /* 0,0,0 */

这意味着包含通用选择器的选择器的特异性不会因通用选择器的存在而改变。以下两个选择器具有完全相同的特异性:

div p         /* 0,0,2 */
body * strong /* 0,0,2 */

对于 :where() 伪类来说也是如此,无论其选择器列表中可能包含哪些选择器。因此,:where(aside#warn, code) 的特异性为0,0,0

组合符号,包括 ~>+ 和空格字符,在选择器的特异性中没有任何影响,甚至零特异性。因此,它们对选择器的总体特异性没有影响。

ID 和属性选择器的特异性

需要注意 ID 选择器与针对 id 属性的属性选择器之间特异性的差异。回到示例代码中的第三对规则,我们发现以下内容:

html > body table tr[id="totals"] td ul > li {color: maroon;} /* 0,1,7 */
li#answer {color: navy;}                                      /* 1,0,1 (wins) */

第二条规则中的 ID 选择器(#answer)对选择器的整体特异性贡献了 1,0,0。然而,在第一条规则中,属性选择器([id="totals"])对选择器的整体特异性贡献了 0,1,0。因此,根据以下规则,具有idmeadow的元素将变为绿色:

#meadow {color: green;}      /* 1,0,0 */
*[id="meadow"] {color: red;} /* 0,1,0 */

重要性

有时声明如此重要,以至于它超越所有其他考虑因素。CSS 称这些为重要声明(希望理由显而易见),并允许您通过在声明的终止分号之前插入!important标志来标记它们:

p.dark {color: #333 !important; background: white;}

在这里,#333的颜色值被标记为!important标志,而white的背景值则没有。如果您希望将两个声明都标记为重要,则每个声明都需要自己的!important标志:

p.dark {color: #333 !important; background: white !important;}

你必须正确放置!important标志,否则声明可能会失效:!important 总是 在声明的末尾,分号之前。在涉及允许包含多个关键字值的属性(如font)时,这一放置尤为关键:

p.light {color: yellow; font: smaller Times, serif !important;}

如果!important被放置在font声明的任何其他地方,整个声明很可能会失效,其样式不会应用。

注意

我们意识到,对于那些来自编程背景的人来说,这个令牌的语法本能地被翻译为“不重要”。无论出于何种原因,感叹号(!)被选择作为重要标志的分隔符,在 CSS 中并不意味着“不”,无论其他语言给予它这个确切的含义。这种关联是不幸的,但我们却不得不接受它。

被标记为!important的声明没有特殊的优先级值,而是被单独考虑,与不重要的声明分开。实际上,所有!important声明被分组在一起,并且在该组内解决特异性冲突。类似地,所有不重要的声明被视为一个组,任何组内的冲突都如前所述。因此,在任何重要和不重要的声明冲突的情况下,重要声明总是胜出(除非用户代理或用户已将相同的属性声明为重要,这将在本章后面看到)。

图 4-2 展示了以下规则和标记片段的结果:

h1 {font-style: italic; color: gray !important;}
.title {color: black; background: silver;}
* {background: black !important;}
<h1 class="title">NightWing</h1>

css5 0402

图 4-2. 重要规则总是胜出
警告

在你的 CSS 中通常不建议使用!important,而且很少需要。如果你发现自己使用了!important,请停下来寻找其他方法来达到同样的效果而不使用!important。级联层是其中之一的可能性;请参阅“按级联层排序”以了解更多详情。

继承

理解样式应用于元素的另一个关键概念是继承。继承是一种机制,使某些样式不仅应用于指定的元素,还应用于其后代。例如,如果将颜色应用于<h1>元素,则该颜色将应用于<h1>内所有文本,甚至是该<h1>子元素内的文本:

h1 {color: gray;}
<h1>Meerkat <em>Central</em></h1>

普通<h1>文本和<em>文本都被着灰色,因为<em>元素继承了<h1>color值。如果属性值不能被后代元素继承,那么<em>文本将是黑色,而不是灰色,我们将不得不单独为这些元素着色。

考虑一个无序列表。假设我们为<ul>元素应用了color: gray;样式:

ul {color: gray;}

我们期望应用于<ul>的样式也将应用于其列表项,以及任何这些列表项的内容,包括标记(即每个列表项旁边的符号)。由于继承的存在,这正是发生的,正如图 4-3 所示。

css5 0403

图 4-3. 样式的继承

通过查看文档的树形图,更容易理解继承的工作方式。图 4-4 展示了与图 4-3 中显示的非常简单文档类似的文档的树形图。

css5 0404

图 4-4. 简单的树形图

当将声明color: gray;应用于<ul>元素时,该元素采用该声明。然后该值向下传播到后代元素,并一直持续到没有更多后代可以继承该值为止。值绝对不会向上传播;元素永远不会将值传递给其祖先。

注意

在 HTML 中,上行传播规则有一个显著的例外:应用于<body>元素的背景样式可以传递给<html>元素,后者是文档的根元素,因此定义了其画布。这仅在<body>元素具有定义的背景且<html>元素没有时发生。少数其他属性也具有此从 body 到 root 的行为,例如overflow,但仅适用于<body>元素。其他元素不会从后代继承属性。

继承是 CSS 中非常基本的内容之一,几乎从不考虑,除非必须处理。然而,仍应牢记几点。

首先,请注意,许多属性不会被继承——通常是为了避免不良结果。例如,border属性(用于为元素设置边框)不会继承。快速查看图 4-5 就可以看出原因。如果边框被继承,文档会变得更加混乱——除非作者额外努力关闭继承的边框。

css5 0405

图 4-5. 为什么边框不会被继承

正如事实上所发生的那样,大多数框模型属性——包括边距、填充、背景和边框——由于同样的原因而不会被继承。毕竟,你可能不希望段落中的所有链接都从其父元素继承一个 30 像素的左边距!

其次,继承的值根本没有任何特异性,甚至是零特异性。这似乎是一种学术上的区别,直到你通过缺乏继承特异性的后果来工作,考虑下列规则和标记片段,并将其与图 4-6 中显示的结果进行比较:

* {color: gray;}
h1#page-title {color: black;}
<h1 id="page-title">Meerkat <em>Central</em></h1>
<p>
Welcome to the best place on the web for meerkat information!
</p>

css5 0406

图 4-6. 零特异性击败无特异性

由于通用选择器适用于所有元素并且具有零特异性,其颜色声明的gray值胜过完全没有特异性的继承值black。(现在你可能理解为什么我们将:where()和通用选择器列为具有0,0,0特异性了:它们不增加权重,但确实匹配元素。)因此,<em>元素呈现为灰色而不是黑色。

这个例子生动地说明了无差别使用通用选择器可能会遇到的潜在问题之一。因为它可以匹配任何元素或伪元素,通用选择器通常会导致继承的短路效应。虽然可以绕过这个问题,但通常更明智的做法是避免使用单独的通用选择器以避免这个问题。

对于继承值的完全缺乏特异性并非一个微不足道的观点。例如,假设样式表已被编写,以便工具栏中的所有文本都是白色的黑色背景:

#toolbar {color: white; background: black;}

只要具有toolbar id 的元素内不包含超链接(a元素)的纯文本,这将起作用。但是,如果此元素内的文本全部都是超链接,那么用户代理样式表中的超链接样式将接管。在 Web 浏览器中,这意味着它们可能会被着蓝色,因为浏览器的内部样式表可能包含类似以下条目:

a:link {color: blue;}

要解决这个问题,你必须声明类似以下内容:

#toolbar {color: white; background: black;}
#toolbar a:any-link {color: white;}

通过直接针对工具栏内的a元素制定规则,你将获得图 4-7 中显示的结果。

css5 0407

图 4-7. 直接为相关元素分配样式

另一种获得相同结果的方法是使用值inherit,在下一章节中进行讨论。我们可以像这样修改前面的例子:

#toolbar {color: white; background: black;}
#toolbar a:link {color: inherit;}

这也导致了图 4-7 中显示的结果,因为color的值由于具有特异性的分配规则而明确继承。

层叠

在本章中,我们回避了一个相当重要的问题:当两个具有相等特异性的规则应用于同一元素时会发生什么?浏览器如何解决冲突?例如,考虑以下规则:

h1 {color: red;}
h1 {color: blue;}

哪个胜出?两者的特异性都是0,0,1,因此它们具有相等的权重,应该都适用。但事实并非如此,因为元素不能既是红色又是蓝色。那么将是哪一个呢?

最后,Cascading Style Sheets的名字开始变得清晰:CSS 是基于一种将样式级联在一起的方法,通过结合继承和特异性以及一些规则实现。CSS 的级联规则如下:

  1. 查找包含与给定元素匹配的选择器的所有规则。

  2. 将应用于给定元素的所有声明按显式权重排序。

  3. 将应用于给定元素的所有声明按来源排序。有三种基本来源:作者、读者和用户代理。在正常情况下,作者的样式(即您作为页面作者的样式)优先于读者的样式,作者和读者的样式都会覆盖用户代理的默认样式。但对于标记为!important的规则,用户代理样式优先于作者样式,而两者都优先于读者样式。

  4. 将应用于给定元素的所有声明按封装上下文排序。例如,如果通过影子 DOM 分配样式,则对于该影子 DOM 中的所有元素都有封装上下文,不适用于影子 DOM 外的元素。这允许封装样式覆盖从影子 DOM 外继承的样式。

  5. 将所有声明按是否元素附加排序。通过style属性分配的样式是元素附加的。通过样式表分配的样式,无论是外部的还是嵌入的,都不是。

  6. 将所有声明按级联层排序。对于普通权重的样式,级联层在 CSS 中首次出现的越晚,优先级越高。没有层的样式被视为“默认”最终伪层的一部分,其优先级高于显式创建层中的样式。对于重要权重的样式,级联层在 CSS 中出现得越,优先级就越高,而显式创建层中的所有重要权重样式都优先于默认层中的样式,无论是否重要。级联层可以出现在任何来源中。

  7. 将应用于给定元素的所有声明按特异性排序。具有较高特异性的元素比具有较低特异性的元素具有更高的权重。

  8. 将应用于给定元素的所有声明按出现顺序排序。声明在样式表或文档中出现得越晚,其权重就越高。被导入样式表中的声明被认为出现在导入它们的样式表中的所有声明之前。

要清楚这一切是如何运作的,让我们考虑一些例子,这些例子说明了一些级联规则。

按重要性和来源排序

如果两条规则适用于一个元素,并且其中一条标记为!important,则重要规则获胜:

p {color: gray !important;}
<p style="color: black;">Well, <em>hello</em> there!</p>

即使段落的style属性中分配了颜色,!important规则仍然占优势,段落会是灰色的。这是因为!important的排序比元素附加样式(style="")的排序具有更高的优先级。灰色也会被<em>元素继承。

请注意,如果在此情况下将!important添加到内联样式中,将获胜。因此,根据以下情况,段落(及其后代元素)将是黑色的:

p {color: gray !important;}
<p style="color: black !important;">Well, <em>hello</em> there!</p>

如果重要性相同,则考虑规则的来源。如果元素同时匹配作者样式表和读者样式表中的普通样式,则使用作者样式。例如,假设以下样式来自指定的来源:

p em {color: black;}    /* author's stylesheet */

p em {color: yellow;}   /* reader's stylesheet */

在这种情况下,段落中的强调文本将是黑色的,而不是黄色的,因为作者样式胜过读者样式。然而,如果两条规则都标记为!important,情况将发生变化:

p em {color: black !important;}    /* author's stylesheet */

p em {color: yellow !important;}   /* reader's stylesheet */

现在段落中的强调文本将是黄色的,而不是黑色的。

正如所述,用户代理的默认样式——通常受用户偏好影响——也被考虑在内。默认样式声明是所有声明中最不具影响力的。因此,如果作者定义了一个适用于锚点的规则(例如,声明它们为white),那么此规则将覆盖用户代理的默认值。

总结一下,CSS 在声明优先级方面有八个基本级别。按照优先级从高到低的顺序,它们如下:

  1. 过渡声明(参见第十八章)

  2. 用户代理重要声明

  3. 读者重要声明

  4. 作者重要声明

  5. 动画声明(参见第十九章)

  6. 作者普通声明

  7. 读者普通声明

  8. 用户代理声明

因此,过渡样式将覆盖所有其他规则,无论这些规则是否标记为!important或规则的来源如何。

按元素附加排序

样式可以通过使用诸如style这样的标记属性附加到元素。这些被称为元素附加样式,它们仅被来源和权重的考虑所超越。

要理解这一点,请考虑以下规则和标记片段:

h1 {color: red;}
<h1 style="color: green;">The Meadow Party</h1>

给定规则应用于<h1>元素,你可能仍然期望<h1>文本是绿色的。这是因为每个内联声明都是元素附加的,因此比不是元素附加的样式(如color: red规则)权重更高。

这意味着即使元素具有与规则匹配的id属性,也会遵守内联样式声明。让我们修改前面的示例以包括一个id

h1#meadow {color: red;}
<h1 id="meadow" style="color: green;">The Meadow Party</h1>

由于内联声明的权重,<h1>元素的文本仍将是绿色的。

只需记住,内联样式通常是不良实践,如果可能的话尽量不要使用它们。

按级联层排序

级联层允许作者将样式分组,以便它们在级联中共享一个优先级别。这听起来有点像!important;在某些方面它们是相似的,但在其他方面则截然不同。这比描述要容易演示。创建级联层的能力意味着作者可以平衡各种需求,例如组件库的需求与网页或 Web 应用的特定部分的需求。

级联层是在 2021 年底引入到 CSS 中的,因此只有从那时起发布的浏览器才支持它们。

如果冲突声明适用于元素,并且所有声明具有相同的显式权重和来源,并且没有元素附加,那么它们将按级联层排序。层的优先顺序由首次声明或使用层的顺序设置,后面声明的层对于普通样式具有比前面声明的层更高的优先级。考虑以下示例:

@layer site {
     h1 {color: red;}
}
@layer page {
     h1 {color: blue;}
}

这些<h1>元素将会被着蓝色色。这是因为在 CSS 中,page层在site层之后,因此具有更高的优先级。

任何不属于命名级联层的样式都会分配给隐式的“默认”层,这一层对于不重要的规则具有比任何命名层更高的优先级。假设我们如下修改之前的示例:

h1 {color: maroon;}
@layer site {
     h1 {color: red;}
}
@layer page {
     h1 {color: blue;}
}

<h1>元素现在将是栗色的,因为隐式“默认”层(h1 {color: maroon;}所属的层)比任何命名层具有更高的优先级。

您还可以为命名级联层定义特定的优先顺序。考虑以下 CSS:

@layer site, page;

@layer page {
   h1 {color: blue;}
}

@layer site {
   h1 {color: red;}
}

在这里,第一行定义了层的优先顺序:对于像示例中显示的普通权重规则,page层将比site层具有更高的优先级。因此,在这种情况下,<h1>元素将是蓝色的,因为在排序层时,pagesite具有更高的优先级。对于标记为重要的规则,优先顺序则相反。因此,如果两个规则都标记为!important,优先级将翻转,<h1>元素将是红色的。

让我们再详细讨论一下级联层的工作原理,特别是因为它们对 CSS 来说是如此新的。假设您想定义三个层:一个用于基本站点样式,一个用于单个页面样式,以及一个用于从外部样式表导入的组件库的样式。CSS 可能如下所示:

@layer site, page;
@import url(/assets/css/components.css) layer(components);

这种排序方式将会使普通权重的components样式覆盖pagesite的普通权重样式,而普通权重的page样式仅会覆盖site的普通权重样式。相反,重要的site样式将会覆盖所有pagecomponents样式,不论它们是重要的还是普通权重的,而重要的page样式将会覆盖所有components样式。

这里有一个小例子,展示了如何管理层:

@layer site, component, page;
@import url(/c/lib/core.css) layer(component);
@import url(/c/lib/widgets.css) layer(component);
@import url(/c/site.css) layer(site);

@layer page {
   h1 {color: maroon;}
   p {margin-top: 0;}
}

@layer site {
   body {font-size: 1.1rem;}
   h1 {color: orange;}
   p {margin-top: 0.5em;}
}

p {margin-top: 1em;}

这个示例包含三个导入的样式表,其中一个分配给了site层,另外两个在component层。然后一些规则分配给了page层,并且有一些规则放在了site层。@layer site {}块中的规则将与/c/site.css中的规则合并成一个单独的site层。

在此之后,有一个层外的显式级联层之外的规则,这意味着它是隐式“默认”层的一部分。默认层中的规则将覆盖任何其他层的样式。因此,根据所示的代码,段落将具有1em的顶部边距。

但在此之前,一个指令设置了命名层的优先顺序:page优先于componentsitecomponent优先于site。以下是这些各种规则在级联方面的分组,带有描述它们在排序中的位置的注释:

/* 'site' layer is the lowest weighted */
@import url(/c/site.css) layer(site);
@layer site {
   body {font-size: 1.1rem;}
   h1 {color: orange;}
   p {margin-top: 0.5em;}
}

/* 'component' layer is the next-lowest weighted */
@import url(/c/lib/core.css) layer(component);
@import url(/c/lib/widgets.css) layer(component);

/* 'page' layer is the next-highest weighted */
@layer page {
   h1 {color: maroon;}
   p {margin-top: 0;}
}

/* the implicit layer is the highest weighted */
p {margin-top: 1em;}

正如您所看到的,层在层次排序中越晚出现,级联排序算法就会给予它们越多的权重。

清楚地说,级联层不一定要有名称。命名只是在设置它们的顺序方面更加清晰,并且还使得可以向层添加样式。这里有一些使用未命名级联层的例子:

@import url(base.css) layer;

p {margin-top: 1em;}

@layer {
   h1 {color: maroon;}
   body p {margin-top: 0;}
}

在这种情况下,从base.css导入的规则分配给了一个未命名层。即使这个层实际上没有名称,我们可以把它看作是 CL1。然后,一个层外的规则将段落的顶部边距设置为1em。最后,一个未命名的层块有一些规则;我们可以把这个层看作是 CL2。

现在我们有了三个层中的规则:CL1、CL2 和隐式层。它们的考虑顺序是这样的,所以在任何冲突的普通规则的情况下,隐式默认层中的规则(在排序中排在最后)将胜出于其他两个层中的冲突规则,CL2 中的规则将胜出于 CL1 中的冲突规则。

至少对于普通权重规则是这样的。对于!important规则,优先级顺序是相反的,所以 CL1 中的规则将胜过其他两个层中的冲突重要规则,CL2 中的重要规则将胜过隐式层中的冲突重要规则。奇怪但是事实!

这种按顺序排序将在稍后再次出现,但首先让我们把特异性引入级联。

按特异性排序

如果冲突声明应用于一个元素,并且这些声明具有相同的显式权重、来源、元素附加(或缺乏附加)和级联层,则它们将按特异性排序。最特异的声明胜出,就像这样:

@layer page {
  p#bright#bright#bright {color: grey;}
}
p#bright {color: silver;}
p {color: black;}
<p id="bright">Well, hello there!</p>

根据这些规则,段落的文本将呈现为银色,如 图 4-8 所示。为什么?因为 p#bright 的特异性(1,0,1)优先于 p 的特异性(0,0,1),尽管后者在样式表中出现得更晚。即使具有最强选择器(3,0,1)的 page 层的样式也没有被比较。只有具有优先级的层中的声明才会竞争。

css5 0408

图 4-8. 更高的特异性胜过更低的特异性

记住,这个规则只适用于规则属于同一个级联层的情况。如果不是,特异性就不重要了:隐式层中的 0,0,1 选择器将优先于显式创建的级联层中的任何不重要规则,无论后者的特异性有多高。

按顺序排序

最后,如果两条规则在显式权重、来源、元素附加、级联层和特异性完全相同,那么在样式表中后出现的规则将获胜,类似于级联层按顺序排序,后续层次胜过早期层次。

让我们回到一个早期的例子,在文档样式表中找到以下两条规则:

body h1 {color: red;}
html h1 {color: blue;}

在这种情况下,文档中所有 <h1> 元素的 color 值将是 blue,而不是 red。这是因为这两条规则在显式权重和来源上是相等的,位于同一个级联层中,并且选择器具有相等的特异性,因此最后声明的规则是赢家。文档树中元素之间的距离有多近并不重要;即使 <body><h1><html><h1> 更接近,后者也是赢家。唯一重要的事情(当来源、级联层、层和特异性相同时)是规则在 CSS 中出现的顺序。

那么如果完全不同的样式表中的规则冲突会发生什么?例如,假设如下:

@import url(basic.css);
h1 {color: blue;}

如果 h1 {color: red;} 出现在 basic.css 中会发生什么?在这种情况下,由于没有级联层参与,整个 basic.css 的内容都被视为粘贴到 @import 发生的样式表的点。因此,文档的样式表中包含的任何规则都比 @import 中的规则晚出现。如果在显式权重和特异性上达到平局,文档的样式表包含的规则将获胜。考虑以下情况:

p em {color: purple;}  /* from imported stylesheet */

p em {color: gray;}    /* rule contained within the document */

在这种情况下,第二条规则胜过了导入规则,因为它是最后一个指定的规则,并且两者都在隐式级联层中。

排序排序是常推荐的链接样式排序背后的原因。推荐的顺序是你应该按照 linkvisitedfocushoveractive 或者 LVFHA 的顺序编写你的链接样式,就像这样:

a:link {color: blue;}
a:visited {color: purple;}
a:focus {color: green;}
a:hover {color: red;}
a:active {color: orange;}

多亏了本章节中的信息,现在你知道所有这些选择器的特异性都是相同的:0,1,1。因为它们都有相同的显式权重、起源和特异性,匹配元素的最后一个将胜出。一个正在点击或通过键盘等方式激活的未访问链接会被四条规则匹配——:link:focus:hover:active——因此这四条规则中的最后一条将胜出。根据 LVFHA 排序,:active 将胜出,这可能是作者想要的结果。

假设你决定忽略常见的顺序并按字母顺序排列你的链接样式。这将得到以下结果:

a:active {color: orange;}
a:focus {color: green;}
a:hover {color: red;}
a:link {color: blue;}
a:visited {color: purple;}

根据这种顺序,没有任何链接会显示:hover:focus:active样式,因为:link:visited规则在其他三个规则之后。每个链接必须是已访问或未访问的,因此这些样式总是会覆盖其他样式。

让我们考虑作者可能想要使用的 LVFHA 顺序的变体。在这种排序中,只有未访问的链接会获得悬停样式;已访问的链接则不会。已访问和未访问的链接都会获得活动样式:

a:link {color: blue;}
a:hover {color: red;}
a:visited {color: purple;}
a:focus {color: green;}
a:active {color: orange;}

这种冲突只有在所有状态尝试设置相同属性时才会发生。如果每个状态的样式涉及不同的属性,则顺序并不重要。在以下情况中,链接样式可以以任何顺序给出,仍将按预期功能运行:

a:link {font-weight: bold;}
a:visited {font-style: italic;}
a:focus {color: green;}
a:hover {color: red;}
a:active {background: yellow;}

你可能也意识到:link:visited样式的顺序并不重要。你可以以 LVFHA 或 VLFHA 的顺序排列这些样式,没有任何负面影响。

能够链式使用伪类消除了所有这些担忧。后两者的特异性大于前两者,因此可以任意顺序列出:

a:link {color: blue;}
a:visited {color: purple;}
a:link:hover {color: red;}
a:visited:hover {color: gray;}

因为每条规则适用于一组唯一的链接状态,它们不会冲突。因此,改变它们的顺序不会改变文档的样式。最后两条规则的特异性确实相同,但这并不重要。悬停的未访问链接不会匹配关于悬停已访问链接的规则,反之亦然。如果我们添加活动状态样式,顺序将再次变得重要。考虑下面这个例子:

a:link {color: blue;}
a:visited {color: purple;}
a:link:hover {color: red;}
a:visited:hover {color: gray;}
a:link:active {color: orange;}
a:visited:active {color: silver;}

如果将活动样式移至悬停样式之前,它们将被忽略。同样,这是因为特异性冲突。可以通过在链中添加更多伪类来避免冲突,如下所示:

a:link:hover:active {color: orange;}
a:visited:hover:active {color: silver;}

这确实提高了选择器的特异性——两者的特异性值均为 0,3,1——但它们并不冲突,因为实际的选择状态是互斥的。链接既不能是访问的悬停活动链接,不能是未访问的悬停活动链接:只有两者之一的规则会匹配。

使用非 CSS 展示性提示进行工作

一个文档可能包含非 CSS 的表现提示,比如已弃用的<font>元素,或者仍然广泛使用的heightwidthhidden属性。这些表现提示会被作者或读者的样式覆盖,但不会被用户代理的样式覆盖。在现代浏览器中,来自 CSS 外部的表现提示被视为属于用户代理的样式表。

总结

层叠样式表(Cascading Style Sheets,CSS)最基本的方面也许就是层叠本身——用于解决冲突声明并确定最终文档呈现的过程。这一过程的核心是选择器的特异性及其关联声明,以及继承机制。

第五章:值和单位

在本章中,我们将讨论几乎可以使用 CSS 做任何事情的基础特性:影响颜色、距离和大小的单位,以及帮助定义这些值的单位。如果没有单位,您将无法声明图像周围应有 10 像素的空白,或者标题文本应该是某个特定大小。通过理解这里提出的概念,您将能够更快地学习和使用 CSS 的其余部分。

关键词、字符串和其他文本值

样式表中的一切都是文本,但某些值类型直接表示文本字符串,而不是数字或颜色。这类别中包括 URL 和非常有趣的是,图像。

关键词

当需要用某种词描述一个值时,CSS 使用关键词。一个常见的例子是关键词 none,它与 0(零)不同。因此,要从 HTML 文档中的链接中移除下划线,您应编写以下内容:

a[href] {text-decoration: none;}

同样地,如果您想要强制链接显示下划线,您将使用关键词 underline 而不是 none

如果属性接受关键词,其关键词将仅在该属性的范围内定义。例如,normalletter-spacing 中的定义与在 font-style 中定义的 normal 意义完全不同。

全局关键词

CSS 定义了五个全局关键词,这些关键词被规范中的每个属性所接受:inheritinitialunsetrevertrevert-layer

inherit

关键词 inherit 使得元素上的属性值与其父元素上的属性值相同。换句话说,它强制执行继承,即使在通常不会操作继承的情况下也是如此。在许多情况下,您不需要指定继承,因为许多属性会自然继承。尽管如此,inherit 仍然很有用。

例如,考虑以下样式和标记:

#toolbar {background: blue; color: white;}

<div id="toolbar">
<a href="one.html">One</a> | <a href="two.html">Two</a> |
<a href="three.html">Three</a>
</div>

<div> 本身将具有蓝色背景和白色前景,但链接将根据浏览器的偏好设置进行样式设置。它们很可能最终成为蓝色文本在蓝色背景上,并在它们之间使用白色垂直条。

您可以编写一个规则,明确将工具栏中的链接设置为白色,但通过使用 inherit 可以使事情更加健壮。您只需将以下规则添加到样式表中:

#toolbar a {color: inherit;}

这将导致链接使用 color 的继承值,而不是用户代理的默认样式。

通常情况下,直接分配的样式会覆盖继承的样式,但 inherit 可以撤消这种行为。这可能并不总是一个好主意——例如,在这里链接可能会与周围的文本融为一体,成为可用性和可访问性的问题——但确实可以做到。

同样,即使通常不会发生,您也可以从父级中拉取属性值下来。例如,border是不会继承的(理所当然)。如果您希望<span>继承其父元素的边框,则只需span {border: inherit;}即可。不过更有可能的是,您只希望<span>的边框使用与其父元素相同的边框颜色。在这种情况下,span {border-color: inherit;}就能达到效果。

initial

关键字initial将属性的值设置为定义的初始值,这在某种程度上意味着它“重置”了该值。例如,font-weight的默认值为normal。因此,声明font-weight: initial与声明font-weight: normal是相同的。

在您考虑并非所有值都有显式定义的初始值之前,这可能看起来有点愚蠢。例如,color的初始值“取决于用户代理”。这不是您应该键入的奇怪关键字!这意味着color的默认值取决于浏览器中的偏好设置之类的东西。虽然几乎没有人将默认文本颜色设置从黑色更改为其他颜色,但某人可能将其设置为深灰色甚至是鲜艳的红色。通过声明color: initial;,您告诉浏览器将元素的颜色设置为用户的默认颜色。

另一个initial的好处是,您可以将属性设置回其初始值,而无需知道该初始值是什么。当您需要一次性重置大量属性时,通过 JavaScript 或 CSS,这尤其有用。

unset

关键字unset充当inheritinitial的通用替代。如果属性是继承的,则unset的效果与使用inherit相同。如果属性是继承的,则unset的效果与使用initial相同。这使得unset在通过取消应用于属性的任何其他样式来重置属性时非常有用。

revert

关键字revert将属性的值设置为如果当前样式来源未进行任何更改,则该属性将具有的值。实际上,revert使您可以说:“此元素的所有属性值应如同作者样式不存在,但用户代理和用户样式存在。”

因此,给定以下基本示例,p元素将呈现为灰色文本,具有透明背景:

p {background: lime; color: gray;}
p {background: revert;}

这意味着任何值被继承的属性将与其父元素的值相同。当您希望去除元素上应用的大量全站样式,并且只想为该元素应用一组单独的样式时,revert关键字就非常有用。与其覆盖所有这些属性,您可以将它们恢复为默认值——您可以通过单个属性all来做到这一点,这是下一节的主题。

revert-layer

如果你正在使用级联层(参见“按级联层排序”)并希望“撤消”当前层可能应用的任何样式,revert-layer值可以帮助。这里的区别在于,revert-layer实际上意味着“这个元素的所有属性值应该就像作者在当前级联层中的样式不存在一样,但其他作者级联层(包括默认层)、用户代理和用户样式存在。”

因此,考虑到以下情况,包含单词exampleclass的段落将呈现为红色文本在黄色背景上:

@layer site, system;

p {color: red;}
@layer system {
	p {background: yellow; color: fuchsia;}
}
@layer site {
	p {background: lime; color: gray;}
	p.example {background: revert; color: revert;}
}

对于背景,浏览器查看前面级联层中的指定值,并选择权重最高的一个。只有一个层级(system)设置了背景颜色,因此使用它而不是lime。前景颜色也是如此,因为在默认层中分配了颜色,并且默认层会覆盖所有显式创建的层级,所以使用red而不是gray

注意

截至 2023 年末,只有 Firefox 支持revert-layer,但我们预计它在不久的将来将得到广泛支持。

all属性

这些全局值可用于所有属性,但有一个特殊属性仅接受全局关键字:all

all属性是所有属性的代表,除了 directionunicode-bidi和任何自定义属性(参见“自定义属性”)。因此,如果你在元素上声明all: inherit,就表示你希望所有属性(除了directionunicode-bidi和自定义属性)从元素的父级继承其值。考虑以下情况:

section {color: white; background: black; font-weight: bold;}
#example {all: inherit;}
<section>
    <div id="example">This is a div.</div>
</section>

你可能会认为这会导致<div>元素从<section>元素继承colorbackgroundfont-weight的值。确实如此,但它还会强制继承 CSS 中每一个其他属性的值(除了两个例外)从<section>元素。

或许这正是你想要的,如果是这样,那太好了。但如果你只是想继承为<section>元素编写的属性值,CSS 应该更像这样:

section {color: white; background: black; font-weight: bold;}
#example {color: inherit; background: inherit; font-weight: inherit;}

在这些情况下,你真正想要的可能是all: unset,但你的样式表可能会有所不同。

字符串

字符串值是一个任意字符序列,用单引号或双引号括起来,在值定义中用<string>表示。这里有两个简单的例子:

"I like to play with strings."
'Strings are fun to play with.'

注意引号的平衡,也就是说,你总是以相同类型的引号开始和结束。如果搞错了,会导致各种解析问题,因为以一种引号开始并试图用另一种引号结束意味着字符串实际上不会被终止。你可能会意外地将后续规则合并到字符串中!

如果你想在字符串内部放置引号,那是可以的,只要它们不是用来封闭字符串的那种引号,或者使用反斜杠进行转义即可:

"I've always liked to play with strings."
'He said to me, "I like to play with strings."'
"It's been said that \"haste makes waste.\""
'There\'s never been a "string theory" that I\'ve liked.'

注意只有'"这两种字符可以作为字符串的定界符,有时被称为直引号。这意味着你不能使用卷曲智能引号来开始或结束字符串值。你可以在字符串值内部使用它们,就像这个代码示例中一样,并且它们不需要被转义:

"It’s been said that “haste makes waste.”"
'There’s never been a “string theory” that I’ve liked.'

这要求你为文档使用 Unicode 编码(使用Unicode 标准),但无论如何你都应该这样做。

如果你有某种原因要在字符串值中包含换行符,可以通过转义换行符本身来实现。CSS 会将其删除,使事情看起来好像从未存在过。因此,从 CSS 的角度看,以下两个字符串值是相同的:

"This is the right place \
for a newline."
"This is the right place for a newline."

另一方面,如果确实需要一个包含换行符的字符串值,请在所需的换行位置使用 Unicode 参考\A

"This is a better place \Afor a newline."

标识符

一词,区分大小写的字符串不应被引用,称为标识符,在 CSS 语法中表示为<ident>或<custom-ident>,具体取决于规范和上下文。标识符用于动画名称、网格线名称和计数器名称等。此外,<dashed-ident> 用于自定义属性。创建自定义标识符的规则包括不以数字、双连字符或单连字符后跟数字开头。除此之外,几乎任何字符都是有效的,包括表情符号,但如果使用特定字符,如空格或反斜杠,则需要用反斜杠进行转义。

标识符本身是单词并且区分大小写;因此,对于 CSS 而言,myIDMyID是完全不同且不相关的。如果属性接受标识符和一个或多个关键字,作者应该注意绝不定义与有效关键字相同的标识符,包括全局关键字initialinheritunsetrevert。使用none也是一个非常糟糕的主意。

URLs

如果你写过网页,几乎肯定熟悉统一资源定位符(URLs)。每当你需要引用一个时,例如在导入外部样式表时使用的@import语句,这里是一般的格式:

url(protocol://*server*/*pathname*/*filename*)
url("*<string>*")   /* can use single or double quotes, or no quotes. */

这个例子定义了一个绝对 URL。无论它在何处(或者更确切地说,在哪个页面)找到,这个 URL 都有效,因为它在 Web 空间中定义了一个绝对位置。假设你有一个名为web.waffles.org的服务器。在该服务器上有一个名为pix的目录,在此目录中有一张名为waffle22.gif的图像。在这种情况下,该图像的绝对 URL 如下:

https://web.waffles.org/pix/waffle22.gif

无论写在何处,这个 URL 都是有效的,无论包含它的页面位于服务器web.waffles.org还是web.pancakes.com

另一种 URL 类型是相对 URL,因为它指定的位置是相对于使用它的文档的位置。如果你引用相对位置,比如与网页相同目录中的文件,一般格式如下:

url(*pathname*)
url("*<string>*")   /* can use single or double quotes. */

这仅在图片位于包含 URL 的页面相同服务器上时有效。假设你有一个网页位于http://web.waffles.org/syrup.html,你希望图片waffle22.gif显示在此页面上。在这种情况下,URL 将是以下内容:

pix/waffle22.gif

此路径有效,因为 Web 浏览器知道应该从找到 Web 文档的地方开始,然后添加相对 URL。在这种情况下,路径名pix/waffle22.gif加到服务器名http://web.waffles.org等于http://web.waffles.org/pix/waffle22.gif。你几乎总是可以在相对 URL 的位置使用绝对 URL;使用哪种都可以,只要它定义了一个有效的位置。

在 CSS 中,相对 URL 是相对于样式表本身而言,而不是使用样式表的 HTML 文档。例如,你可能有一个导入另一个样式表的外部样式表。如果你使用相对 URL 导入第二个样式表,它必须相对于第一个样式表。实际上,如果在任何导入的样式表中有 URL,它都需要相对于导入的样式表。

举个例子,考虑一个 HTML 文档位于http://web.waffles.org/toppings/tips.html,它有一个指向样式表http://web.waffles.org/styles/basic.css<link>

<link rel="stylesheet" type="text/css"
    href="http://web.waffles.org/styles/basic.css">

在文件basic.css中有一个@import语句,指向另一个样式表:

@import url(special/toppings.css);

这个@import将导致浏览器查找样式表http://web.waffles.org/styles/special/toppings.css,而不是http://web.waffles.org/toppings/special/toppings.css。如果你有一个位于后者位置的样式表,在basic.css中的@import应该以下面两种方式之一书写:

@import url(http://web.waffles.org/toppings/special/toppings.css);

@import url("../special/toppings.css");

注意,url和左括号之间不能有空格:

body {background: url(http://www.pix.web/picture1.jpg);}   /* correct */
body {background: url  (images/picture2.jpg);}          /* INCORRECT */

如果存在空格,则整个声明将无效,因此被忽略。

注意

截至 2022 年末,CSS 工作组计划引入一个名为src()的新函数,它将仅接受字符串而不接受未引用的 URL。这旨在允许在src()内使用自定义属性,这将允许作者基于自定义属性的值定义应加载哪个文件。

图像

image value是对图像的引用,正如你可能猜到的那样。其语法表示是<image>。

在支持的最基本级别,也就是说,地球上每个 CSS 引擎都能理解的级别上,一个<image>值是一个<url>值。在更现代的用户代理中,<image>代表以下之一:

<url>

外部资源的 URL 标识符,例如图片的 URL。

<gradient>

可以是线性、径向或锥形渐变图像,单独或重复模式。渐变相当复杂,详细介绍见第九章。

<image-set>

一组图像,根据嵌入到值中的一组条件选择,该值定义为image-set(),但更广泛地识别为带有-webkit-前缀。例如,-webkit-image-set()可以指定在桌面布局中使用较大的图像,而在移动设计中使用较小的图像(无论是像素大小还是文件大小)。该值旨在至少近似于<picture>元素的srcset属性的行为。截至 2023 年初,-webkit-image-set基本上被所有浏览器支持,除了 Safari 之外的大多数浏览器也接受image-set()(无前缀)。

<cross-fade>

用于将两个(或更多)图像混合在一起,并给每个图像指定特定的透明度。用例包括将两个图像混合在一起,将图像与渐变混合等。截至 2023 年初,这在基于 Blink 和 WebKit 的浏览器中作为-webkit-cross-fade()支持,但在 Firefox 系列中无论是否带有前缀都不支持。

还有image()element()函数,但截至 2023 年初,除了 Firefox 支持的带有供应商前缀的element()版本外,任何浏览器都不支持。最后,paint()指的是由 CSS Houdini 的 PaintWorklet 绘制的图像。截至 2023 年初,这只在像 Chrome 这样的基于 Blink 的浏览器中以基本形式支持。

数字和百分比

数字和百分比是许多其他值类型的基础。例如,可以使用em单位(本章后面讨论)前面带有数字来定义字体大小。但这里的数字是什么类型?理解这里的数字类型将帮助您更好地掌握定义其他值类型的方法。

整数

整数值就是这么简单:一个或多个数字,可选择地由+-(正或负)符号前缀表示正数或负数值。就是这样。整数值在值语法中表示为<integer>。例如,71213−421066

一些属性定义了可接受整数值范围。默认情况下,超出定义范围的整数值被视为无效,并导致整个声明被忽略。但是,某些属性定义了行为,使超出接受范围的值被设置为最接近声明值的接受值,称为夹紧

在没有限制范围的情况下(例如属性z-index),用户代理必须支持值范围达到±1,073,741,824(±2³⁰)。

数字

数字值 可以是一个<integer>或者实数,即整数后跟一个点,然后是一些后续整数。此外,它可以由 +- 前缀以指示正数或负数值。数字值在值语法中表示为<number>。例如,52.7183−3.14166.28321.0218e29(科学记数法)。

数字值 可以是<integer>,但它们是不同的值类型,因为某些属性仅接受整数(例如 z-index),而其他属性接受任何实数(例如 flex-grow)。

与整数值一样,数字值可能受属性定义的限制;例如,opacity 限制其值为范围在 01 之间的任何有效<number>。一些属性定义了行为,使得超出接受范围的值被夹紧到最接近声明值的可接受值;例如,opacity: 1.7 将被夹紧为 opacity: 1。对于不这样做的属性,超出定义范围的数字值被视为无效,并导致整个声明被忽略。

百分比

百分比值 是一个<number> 后跟百分号(%),在值语法中表示为<percentage>。例如,50%33.333%。百分比值始终相对于另一个值,可以是任何值——同一元素的另一个属性的值,从父元素继承的值,或祖先元素的值。接受百分比值的属性将定义允许的百分比值范围以及相对计算百分比的方式。

分数

分数值(或 flexible ratio)是一个<number> 后跟 fr 单位标签。因此,一个分数单位是 1frfr 单位表示网格容器中剩余空间(如果有)的一部分。

与所有 CSS 尺寸一样,单位和数字之间没有空格。分数值不是长度(与某些<percentage>值不兼容),因此不能与 calc() 函数中的其他单位类型一起使用。

注意

分数值主要用于网格布局(参见 第十二章),但计划将其用于更多上下文,例如计划中(截至 2023 年初)的 stripes() 函数。

距离

许多 CSS 属性,如边距,依赖于长度测量来正确显示各种页面元素。因此,毫不奇怪,CSS 提供了多种测量长度的方式。

所有长度单位都可以表示为正数或负数后跟标签,尽管某些属性仅接受正数。您还可以使用实数,即带有小数部分的数字,例如 10.5 或 4.561。

All length units are followed by a short abbreviation that represents the actual unit of length being specified, such as in (inches) or pt (points). The only exception to this rule is a length of 0 (zero), which need not be followed by a unit when describing lengths.

这些长度单位分为两种类型:绝对长度单位相对长度单位

绝对长度单位

We’ll start with absolute units because they’re easiest to understand. The seven types of absolute units are as follows:

英寸 (in)

正如您所预期的那样,这个表示法指的是您在美国标尺上找到的英寸。尽管几乎整个世界使用公制系统,但这个单位在规范中的存在是对美国在互联网上普遍存在的一个有趣见解——但现在我们不要深入虚拟社会政治理论。

厘米 (cm)

指的是您在全世界标尺上找到的厘米。一英寸等于 2.54 厘米,一厘米等于 0.394 英寸。

毫米 (mm)

对于那些对公制系统不熟悉的美国人来说,1 厘米等于 10 毫米,因此 1 英寸等于 25.4 毫米,而 1 毫米等于 0.0394 英寸。

四分之一毫米 (Q)

一厘米中有 40Q 单位;因此,将一个元素设置为 1/10 厘米宽度(也就是 1 毫米宽度)将意味着一个值为4Q

(pt)

Points are standard typographical measurements that have been used by printers and typesetters for decades and by word processing programs for many years. Traditionally, there are 72 points to an inch. Therefore the capital letters of text set to 12 points should be one-sixth of an inch tall. For example, p {font-size: 18pt;} is equivalent to p {font-size: 0.25in;}.

派卡 (pc)

一派卡(pica)是另一个排版术语,等于 12 点,这意味着一英寸中有 6 派卡。正如刚才展示的那样,将文本设置为 1 派卡的大写字母应该是英寸高的六分之一。例如,p {font-size: 1.5pc;} 将文本设置为与定义点示例中的示例声明相同的大小。

像素 (px)

A pixel is a small box onscreen, but CSS defines pixels more abstractly. In CSS terms, a pixel is defined to be the size required to yield 96 pixels per inch. Many user agents ignore this definition in favor of simply addressing the pixels on the screen. Scaling factors are brought into play when page zooming or printing, where an element 100px wide can be rendered more than 100 device dots wide.

这些单位仅在浏览器知道显示页面的屏幕所有详细信息、正在使用的打印机或其他可能应用的用户代理时才真正有用。在 Web 浏览器上,显示受屏幕大小和屏幕设置的分辨率影响;作为作者,您对这些因素无能为力。如果没有其他办法,测量应保持相对一致——即,1.0in 的设置应比 0.5in 大一倍,如图 5-1 所示。

css5 0501

图 5-1. 设置绝对长度的左边距

让我们(相当可疑地)假设您的计算机足够了解其显示系统,以准确地再现现实世界的测量。在这种情况下,您可以通过声明 p {margin-top: 0.5in;} 来确保每个段落的顶部边距为半英寸。

绝对单位在定义样式表用于印刷文档时非常有用,这些单位通常以英寸、点和 Picas 为单位测量事物。

像素长度

表面上看,像素是直接的。如果你仔细看屏幕,你会发现它被划分成许多小方块的网格。每个方块就是一个像素。比如说,你定义一个元素是特定数量的像素高和宽,就像下面的标记中所示:

<p>
The following image is 20 pixels tall and wide: <img src="test.gif"
  style="width: 20px; height: 20px;" alt="" />
</p>

那么,该元素将显示在多少屏幕元素高和宽,如图 5-2 所示。

css5 0502

图 5-2. 使用像素长度

问题在于,由于像移动设备和现代笔记本电脑上发现的高密度显示器,个体屏幕元素不再被视为像素。相反,CSS 中使用的像素被转换成符合人类期望的某种东西,这将在下一节中详细讨论。

像素理论

在讨论像素时,CSS 规范建议,当显示的分辨率密度与 96 像素每英寸(ppi)显著不同时,用户代理应将像素测量缩放到参考像素。

W3C参考像素 定义如下:

具有设备像素密度为 96dpi 和读者距离为手臂长度的设备上的一个像素的视觉角。对于标称手臂长度为 28 英寸,因此视觉角约为 0.0213 度。对于手臂长度的阅读,1px 相当于约 0.26 mm(1/96 英寸)。

在大多数现代显示器上,每英寸的实际像素数(ppi)比 96 更高——有时甚至更高。例如,iPhone 13 的 Retina 显示屏物理上是 326ppi,iPad Pro 的显示屏物理上是 264ppi。只要这些设备中的浏览器将参考像素设置为使设定为10px高的元素在屏幕上看起来高 2.6 毫米,物理显示密度就不是你需要担心的事情,就像不需要担心打印品的每英寸点数一样。

分辨率单位

一些单位类型基于显示分辨率:

每英寸的点数 (dpi)

每英寸线性显示点数。这可以是纸打印机输出上的点、LED 屏幕或其他设备上的物理像素,或者电子墨水显示器(如 Kindle)上的元素。

每厘米的点数 (dpcm)

dpi相同,不同之处在于线性测量是 1 厘米而不是 1 英寸。

每像素单位的点数 (dppx)

每个 CSS px单位的显示点数,其中1dppx相当于96dpi,因为 CSS 以这个比例定义像素单位。只要记住,这个比例在 CSS 的未来版本中可能会发生变化。

这些单位最常用于媒体查询的上下文中。例如,作者可以创建一个媒体块,仅在具有高于 500dpi 的显示器上使用:

@media (min-resolution: 500dpi) {
    /* rules go here */
}

再次强调,重要的是要记住 CSS 像素并非设备分辨率像素。设置font-size: 16px的文本在设备具有 96dpi 或 470dpi 时大小相对一致。当设备的 dpi 超过 96 时,通过扩展 CSS 像素来创建缩放,图像将会显得更大,但实际上图像的大小并没有改变:相反,参考像素的屏幕宽度变窄了。

相对长度单位

相对单位之所以被称为如此,是因为它们是相对于其他事物进行测量的。它们测量的实际(或绝对)距离可能会因为超出它们控制之外的因素而改变,比如屏幕分辨率、视区宽度、用户的偏好设置,以及其他一系列因素。另外,对于某些相对单位,它们的大小几乎总是相对于使用它们的元素,因此会随元素的变化而变化。

首先,让我们考虑基于字符的长度单位,包括emexch,它们是密切相关的。另外两个与字体相关的单位,capic,在本章后面讨论。

em 单位

在 CSS 中,1em被定义为给定字体的font-size的值。如果一个元素的font-size是 14 像素,那么对于该元素,1em等于 14 像素。

正如你可能猜到的那样,此值可以根据元素而变化。例如,假设你有一个字体大小为 24 像素的<h1>,一个字体大小为 18 像素的<h2>元素和一个字体大小为 12 像素的段落。如果你将它们的左边距都设置为1em,它们的左边距将分别为 24 像素、18 像素和 12 像素:

h1 {font-size: 24px;}
h2 {font-size: 18px;}
p {font-size: 12px;}
h1, h2, p {margin-left: 1em;}
small {font-size: 0.8em;}
<h1>Left margin = <small>24 pixels</small></h1>
<h2>Left margin = <small>18 pixels</small></h2>
<p>Left margin = <small>12 pixels</small></p>

另一方面,在设置字体大小时,em 的值相对于父元素的字体大小,如图 5-3 所示。

css5 0503

图 5-3. 使用em 为边距和字体大小

理论上,1em 等于所使用字体中小写字母m的宽度——实际上就是这个名称的来源。这是一个老排版工的术语。然而,在 CSS 中不能保证这一点。

ex 单位

ex 单位指的是所使用字体中小写字母x的高度。因此,如果两个段落使用的文本大小为 24 点,但每个段落使用不同的字体,则ex 的值可能对于每个段落都不同。这是因为不同的字体对于x的高度不同,正如你可以在图 5-4 中看到的那样。即使这些示例使用的是 24 点的文本——因此每个示例的em 值为 24 点——每个段落的 x 高度也是不同的。

css5 0504

图 5-4. 变化的 x 高度

ch 单位

ch 单位的广义含义是一个字符CSS Values and Units Level 4ch 的定义如下:

等于在用于渲染它的字体中找到的“0”(零,U+0030)字形的进距度。

“进距度”这个术语是 CSS 的一个词汇,对应于西方排版术语中的“进距宽度”。CSS 使用“测量”一词,因为有些脚本不是从左到右或从右到左,而是从上到下或从下到上,因此可能有进高度而不是进宽度。

不深入细节,字符字形的进距是从一个字符字形的起点到下一个字符字形的起点的距离。这通常对应于字形本身的宽度加上两侧的任何内置间距(尽管该内置间距可以是正的也可以是负的)。

演示 ch 单位的最简单方法是连续运行一堆零,然后设置一个图像的宽度,其单位数与零的数量相同的ch 单位,如图 5-5 所示:

img {height: 1em; width: 25ch;}

在像 Courier 这样的等宽字体中,所有字符根据定义都是1ch 宽。在任何比例面型字体中(这是绝大多数西方字体的类型),字符可能比 0 更宽或更窄,因此不能假定其宽度正好为1ch

css5 0505

图 5-5. 字符相关大小

其他相对长度单位

我们还有几个其他相对长度单位要提到:

ic

水字形(中、日、韩水象形文字 U+6C34)首次在能够呈现它的字体中找到。这类似于 ch,因为它使用了一个水字形的测量,但定义的测量比字符 0 对象语言更有用。如果在特定情况下无法计算 ic,则假定它等于 1em

cap

Cap 高度大约等于大写拉丁字母的高度,即使在不包含拉丁字母的字体中也是如此。如果在特定情况下无法计算,假定它等于字体的上升高度。

lh

等于应用它的元素的 line-height 属性的计算值。

在撰写本文时,只有 Firefox 支持 cap,而只有基于 Chromium 的浏览器支持 lh

根相对长度单位

大多数在前一节讨论的基于字符的长度单位都有对应的根相对值。根相对值是相对于文档的根元素计算的,因此无论在什么上下文中使用,它都提供统一的值。我们将讨论最广泛支持的此类单位,然后总结其余部分。

rem 单位

rem 单位是根据文档根元素的字体大小计算的。在 HTML 中,这是 <html> 元素。因此,声明任何元素具有 font-size: 1rem; 将使其具有与文档根元素相同的字体大小值。

例如,考虑以下标记片段。它将显示在 Figure 5-6 中所示的结果:

<p> This paragraph has the same font size as the root element thanks to
    inheritance.</p>
<div style="font-size: 30px; background: silver;">
  <p style="font-size: 1em;">This paragraph has the same font size as its parent
     element.</p>
  <p style="font-size: 1rem;">This paragraph has the same font size as the root
     element.</p>
</div>

css5 0506

图 5-6. 使用 em 单位(中间句子)与 rem 单位(底部)

实际上,rem 作为字体大小的重置作用:无论对元素的祖先应用了何种相对字体大小,给它 font-size: 1rem; 都会将其放回到根元素设置的位置。这通常是用户的默认字体大小,除非您(或用户)已将根元素设置为特定的字体大小。

例如,给定此声明,1rem 将始终等同于 13px

html {font-size: 13px;}

然而,给定 声明,1rem 将始终等同于用户默认字体大小的四分之三:

html {font-size: 75%;}

在这种情况下,如果用户默认设置为 16 像素,1rem 将等于 12px。如果用户将其默认设置为 12 像素(是的,有些人这样做了),那么 1rem 将等于 9px。如果默认设置为 20 像素,则 1rem 等于 15px。依此类推。

您不限于值 1rem。任何实数都可以使用,就像 em 单位一样,因此您可以像设置所有标题为根元素字体大小的倍数一样玩得开心:

h1 {font-size: 2rem;}
h2 {font-size: 1.75rem;}
h3 {font-size: 1.4rem;}
h4 {font-size: 1.1rem;}
h5 {font-size: 1rem;}
h6 {font-size: 0.8rem;}
注意

只要未为根元素设置字体大小,font-size: 1rem 等同于 font-size: initial

其他根相对单位

如前所述,rem 并非 CSS 定义的唯一根相对单位。 Table 5-1 总结了其他根相对单位。

表格 5-1。根相对等效单位

长度 根相对单位 相对于
em rem 计算的font-size
ex rex 计算的 x 高度
ch rch 0字符的进阶测量
cap rcap 罗马大写字母的高度
ic ric 水字的进阶测量
lh rlh 计算的line-height

在所有的根相对单位中,截至 2022 年后,只有rem受到支持,但几乎所有浏览器都支持它。

视口相对单位

CSS 提供了六个视口相对大小单位。这些单位是相对于视口的大小进行计算的——包括浏览器窗口、可打印区域、移动设备显示等:

视口宽度单位vw

等于视口宽度除以 100。因此,如果视口宽度为 937 像素,1vw等于9.37px。如果视口宽度变化(例如通过拖动浏览器窗口调整宽度),vw的值也会相应变化。

视口高度单位vh

等于视口高度除以 100。因此,如果视口高度为 650 像素,1vh等于6.5px。如果视口高度变化(例如通过拖动浏览器窗口调整高度),vh的值也会相应变化。

视口块单位vb

等于视口沿着块轴的尺寸除以 100。块轴在第六章中有解释。在从上到下书写的语言中,例如英语或阿拉伯语,vb默认与vh相等。

视口内联单位vi

等于视口沿着内联轴的尺寸除以 100。内联轴在第六章中有解释。在水平书写的语言中,例如英语或阿拉伯语,vi默认与vw相等。

视口最小单位vmin

等于视口宽度或高度的 1/100,以较小者为准。因此,如果视口宽度为 937 像素,高度为 650 像素,则1vmin等于6.5px

视口最大单位vmax

等于视口宽度或高度的 1/100,以较大者为准。因此,如果视口宽度为 937 像素,高度为 650 像素,则1vmax等于9.37px

因为这些是像任何其他长度单位一样的长度单位,所以可以在任何允许长度单位的地方使用。例如,你可以通过类似h1 {font-size: 10vh;}的方法,按照视口高度的比例来调整标题的字体大小。这种技术对于文章标题等可能非常有用。

这些单位特别适用于创建全视口界面,例如我们在移动设备上期望找到的界面,因为这些单位允许元素相对于视口而不是文档树中的任何元素进行大小调整。因此,非常简单地填充整个视口,或者至少填充其主要部分,而不必担心任何特定情况下实际视口的精确尺寸。

视口相对大小的基本示例如图 5-7 所示:

div {width: 50vh; height: 33vw; background: gray;}

有趣的(虽然可能不实用)关于这些单位的事实是它们不限于自己的主轴。因此,例如,您可以声明width: 25vh;使元素的宽度等于视口高度的四分之一。

css5 0507

图 5-7. 视口相对大小

这些单位的变体适应视口的变化和它们可能的大小调整,特别是在用户界面可能根据用户输入展开和收缩的设备上。这些变体基于四种视口类型:

默认

用户代理(浏览器)定义的默认视口大小。预期此视口类型对应于单位vwvhvbvivminvmax。默认视口可能对应于其他视口类型之一;例如,默认视口可以与大视口相同,但这取决于每个浏览器的决定。

在任何用户代理界面收缩到其最大程度后的最大可能视口。例如,在移动设备上,浏览器的外壳(浏览器的地址栏、导航栏等)可能大部分时间处于最小化或隐藏状态,以便最大化屏幕区域用于显示页面内容。这是大视口描述的状态。如果您希望元素的大小由整个视口区域决定,即使这样会导致它被界面重叠,大视口单位也是选择的方式。与此视口类型对应的单位包括lvwlvhlvblvilvminlvmax

在任何用户代理界面扩展到其最大程度后,视口的最小可能大小。在这种状态下,浏览器的外壳会尽可能占据屏幕空间,留下最小的页面内容空间。如果您希望确保元素的大小考虑到任何可能的界面操作,请使用这些单位。与此视口类型对应的单位包括svwsvhsvbsvisvminsvmax

动态

内容可见区域,随着 UI 的扩展或收缩而变化。例如,考虑浏览器界面在移动设备上如何根据内容滚动或用户点击屏幕上的位置而显示或消失。如果您希望基于视口大小在每个时刻设置长度,无论其如何变化,那么这些单位就是您需要的。对应于此视口类型的单位包括dvwdvhdvbdvidvmindvmax

截至 2022 年末,滚动条(如果有)在计算所有先前单位时将被忽略。因此,如果滚动条出现或消失,svwdvw 的计算大小将不会改变,或者至少不应该改变。

函数值

CSS 中最近的一些发展之一是有效函数数量的增加。这些值可以从执行数学计算到限制值范围再到从 HTML 属性中提取值。CSS 实际上有很多这些功能,列在这里:

  • abs()

  • acos()

  • annotation()

  • asin()

  • atan()

  • atan2()

  • attr()

  • blur()

  • brightness()

  • calc()

  • character-variant()

  • circle()

  • clamp()

  • color-contrast()

  • color-mix()

  • color()

  • conic-gradient()

  • contrast()

  • cos()

  • counter()

  • counters()

  • cross-fade()

  • device-cmyk()

  • drop-shadow()

  • element()

  • ellipse()

  • env()

  • exp()

  • fit-content()

  • grayscale()

  • hsl()

  • hsla()

  • hue-rotate()

  • hwb()

  • hypot()

  • image-set()

  • image()

  • inset()

  • invert()

  • lab()

  • lch()

  • linear-gradient()

  • log()

  • matrix()

  • matrix3d()

  • max()

  • min()

  • minmax()

  • mod()

  • oklab()

  • oklch()

  • opacity()

  • ornaments()

  • paint()

  • path()

  • perspective()

  • polygon()

  • pow()

  • radial-gradient()

  • rem()

  • repeat()

  • repeat-conic-gradiant()

  • repeating-linear-gradiant()

  • repeating-radial-gradient()

  • rgb()

  • rgba()

  • rotate()

  • rotate3d()

  • rotateX()

  • rotateY()

  • rotateZ()

  • round()

  • saturate()

  • scale()

  • scale3d()

  • scaleX()

  • scaleY()

  • scaleZ()

  • sepia()

  • sign()

  • sin()

  • skew()

  • skewX()

  • skewY()

  • sqrt()

  • styleset()

  • stylistic()

  • swash()

  • symbols()

  • tan()

  • translate()

  • translate3d()

  • translateX()

  • translateY()

  • translateZ()

  • url()

  • var()

这些是 97 个不同的函数值。我们将在本章的其余部分中覆盖一些。其余部分根据其主题在其他章节中进行了描述(例如,过滤函数在第二十章中有详细说明)。

计算值

当您需要进行少量数学运算时,CSS 提供了 calc() 值。在括号内,您可以构建简单的数学表达式。允许的运算符包括+(加法)、-(减法)、*(乘法)和/(除法),以及括号。这些遵循传统的优先级顺序:括号、指数、乘法、除法、加法和减法(PEMDAS),尽管在这种情况下实际上只是 PMDAS,因为 calc() 不允许指数。

例如,假设您希望段落的宽度比其父元素的宽度小 2 em,这是如何用 calc() 表示的:

p {width: calc(90% - 2em);}

calc() 值可与允许以下值类型之一的属性一起使用:<length>、<frequency>、<angle>、<time>、<percentage>、<number> 或 <integer>。您还可以在 calc() 值中使用所有这些单位类型,尽管 CSS 有一些限制需要牢记。

基本限制是 calc() 进行基本类型检查,以确保单位在效果上是兼容的。检查的工作方式如下:

  1. +- 符号的两侧,两个值必须具有相同的单位类型,或者一个是 <number>,另一个是 <integer>(此时结果为 <number>)。因此,5 + 2.7 是有效的,并且结果是 7.7。另一方面,5em + 2.7 是无效的,因为一侧是长度单位,而另一侧不是。注意,5em + 20px 有效的,因为 empx 都是长度单位。

  2. 给定一个 *,参与的值中必须有一个是 <number>(请记住,这包括整数值)。因此 2.5rem * 22 * 2.5rem 都是有效的,每个结果为 5rem。另一方面,2.5rem * 2rem 有效,因为结果将是 5rem²,而长度单位不能是面积单位。

  3. 给定 /,右侧的值必须是一个 <number>。如果左侧是一个 <integer>,则结果是一个 <number>。否则,结果是左侧使用的单位类型。这意味着 30em / 2.75 是有效的,但 30 / 2.75em 是无效的。

  4. 此外,任何导致除零的情况都是无效的。这在像 30px/0 这样的情况中最容易看出,但还有其他导致这种情况的方法。

另一个显著的限制是 +- 运算符两侧都需要空白符,而对于 */ 则不需要。这样做是为了允许未来开发 calc() 值以支持包含连字符的关键字(例如 max-content)。

此外,嵌套 calc() 函数是有效的(也受支持)。因此,您可以这样说:

p {width: calc(90% - calc(1em + 0.1vh));}

超出这个限制,CSS 规范要求用户代理支持任何单个 calc() 函数内的最少20个术语,其中术语可以是数字、百分比或维度(例如长度)。如果术语数量超过用户代理的限制,整个函数将被视为无效。

最大值

计算很好,但有时您只想确保属性设置为多个值中的最小值之一。在这些情况下,min() 函数值非常方便。是的,这一开始可能令人困惑,但请给我们一点时间,希望一切会有所理解。

假设你想确保某个元素永远不超过某个宽度;比如说,一张图像应该是视口宽度的四分之一或宽度为 200 像素中的较小值。这使得它在宽视口上被限制为 200 像素的宽度,但在较小视口上可以占据视口宽度的四分之一。为此,你可以这样说:

.figure {width: min(25vw, 200px);}

浏览器将计算25vw的宽度并将其与200px比较,并使用较小者。如果200px小于视口宽度的 25%,那么将使用200px。否则,元素将宽度为视口宽度的 25%,这可能比1em还要小。请注意,在这种情况下,较小意味着接近负无穷大,而不是接近零。因此,如果比较两个计算结果为(比如)-1500px-2pxmin()将选择-1500px

你可以在min()内嵌套min(),或为其中一个值放入数学表达式,而无需将其包装在calc()中。此外,你还可以加入max()clamp(),即使我们还没有讨论它们。你可以提供尽可能多的项:如果你想比较四种方式来测量某些东西,然后选择最小值,只需用逗号分隔它们。这里有一个略显牵强的例子:

.figure {width: min(25vw, 200px, 33%, 50rem - 30px);}

这些值中计算出的最小值(接近负无穷大)将被使用,从而定义width值的最大值。无论你在函数中的哪个位置列出它们,都不重要,因为始终会选择最小值。

通常情况下,min()可以在允许<length>, <frequency>, <angle>, <time>, <percentage>, <number>, 或 <integer>的任何属性值中使用。

警告

记住,在设置字体大小的最大值时是一个辅助功能的考虑。你不应该使用像素来设置最大字体大小,因为这可能会阻止用户进行文本缩放。在任何情况下,你可能都不应该使用min()来设置字体大小,但如果你使用了,记住在值中不要使用px长度!

最小值

min()的镜像是max(),它可以用于为属性设置最小值。它可以出现在相同的位置并以相同的方式嵌套在min()中,通常情况下与min()几乎相同,只是它选择给定选项中的最大值(接近正无穷大)。

例如,也许页面设计的顶部应该至少 100 像素高,但如果条件允许,它可以更高。在这种情况下,你可以使用类似于这样的东西:

header {height: max(100px, 15vh, 5rem);}

无论这些值中哪个最大,都将被使用。对于桌面浏览器窗口来说,可能会是15vh,除非基本文本大小真的很大。对于手持设备显示,更可能是5rem100px中的最大值。实际上,这设置了一个最小高度为 100 像素,因为将15vh5rem调整到低于该值是很容易的。

记住,即使在字体大小上设置了最小值,也可能会产生无障碍问题,因为太小的最小值仍然太小。处理这个问题的好方法是始终在字体大小的max()表达式中包含1rem。可以使用以下方法:

.sosumi {font-size: max(1vh, 0.75em, 1rem);}

或者,你完全可以不使用max()来设置字体大小。最好将其留给盒模型和其他类似用途。

限制数值

如果你已经考虑过如何嵌套min()max()来设置值的上下限,那么不仅可以这样做,还可以设置一个“理想”值:clamp()。这个函数接受三个参数,依次表示最小允许值、首选值和最大允许值。

例如,考虑一些文本,你希望其大小约为视口高度的 5%,同时保持其最小为基本字体大小,最大为周围文本的三倍。可以这样表达:

footer {font-size: clamp(1rem, 2vh, 3em);}

假设这些样式并假设基本字体大小为 16 像素,大多数浏览器默认设置为这个值,那么页脚文本将等于视口高度为 800 像素(16 除以 0.02)。如果视口变得更高,文本将开始变大,除非这样做会使其大于3em。如果文本的大小达到了3em,它将停止增长。(这种情况相对不太可能,但从未可知。)

如果clamp()的最大值计算结果小于最小值,则忽略最大值,改为使用最小值。

你可以在任何可以使用min()max()的地方使用clamp(),包括在彼此嵌套的情况下。例如:

footer {font-size: clamp(1rem, max(2vh, 1.5em), 3em);}

这与先前的示例基本相同,只是在这种情况下,首选值要么是视口高度的 2%或父元素文本大小的 1.5 倍,取两者中较大的值。

属性值

在一些 CSS 属性中,可以使用attr()函数拉取为元素样式定义的 HTML 属性的值。

例如,在生成内容中,可以插入任何属性的值。它看起来像这样(不必担心理解确切的语法,我们将在第十六章中探讨):

p::before {content: "[" attr(id) "]";}

该表达式将前缀应用于具有id属性的任何段落,该值在方括号中,表示该id的值。因此,将前述样式应用于以下段落将得到图 5-8 中显示的结果:

<p id="leadoff">This is the first paragraph.</p>
<p>This is the second paragraph.</p>
<p id="conclusion">This is the third paragraph.</p>

css5 0508

图 5-8. 插入属性值

虽然attr()content属性值中受支持,但并不被解析。换句话说,如果attr()从属性值返回一个图像 URL,生成的内容将是作为文本写出的 URL,而不是该 URL 上存在的图像。至少在 2022 年末是这样的;已经有计划对attr()进行更改,使其可以被解析(并且可以用于所有属性,而不仅限于content)。

颜色

初学者在创建网页时经常问的第一个问题是:“如何设置页面的颜色?”在 HTML 中,你有两种选择:可以使用一些具有名称的大量但有限的颜色之一,比如红色紫色,或者使用十六进制代码这种稍微神秘的方法。这两种描述颜色的方法都在 CSS 中保留了,并且还有几种我们认为更直观的方法。

命名颜色

多年来,CSS 添加了一组 148 种颜色,这些颜色由像红色火砖红这样的人类可读名称标识。CSS 称这些逻辑上是命名颜色。在早期,CSS 仅使用 HTML 4.01 中定义的 16 种基本颜色关键字:

  • 水蓝

  • 灰色

  • 海军蓝

  • 银色

  • 黑色

  • 绿色

  • 橄榄色

  • 水鸭蓝

  • 蓝色

  • 石灰色

  • 紫色

  • 白色

  • 紫红色

  • 栗色

  • 红色

  • 黄色

所以,假设你想要所有一级标题都是栗色。最好的声明应该是:

h1 {color: maroon;}

很简单,不是吗?图 5-9 显示了更多示例:

h1 {color: silver;}
h2 {color: gray;}
h3 {color: black;}

css5 0509

图 5-9。命名颜色

你可能已经看到(甚至使用过)除了之前列出的那些名称以外的颜色名称。例如,你可以说

h1 {color: lightgreen;}

并将淡绿色(但不完全是石灰色)应用于<h1>元素。

CSS 颜色规范在一个长列表中包括了最初的 16 个命名颜色和 148 个颜色关键字。这个扩展列表基于几十年来使用的标准 X11 RGB 值,并且被浏览器多年来认可,还添加了一些来自 SVG 的颜色名称(主要涉及“灰色”的变体)和一个纪念色。

颜色关键字

CSS 有两个特殊关键字,可以在允许颜色值的任何地方使用:透明currentcolor

正如其名称所示,透明定义了一种完全透明的颜色。CSS 颜色模块将其定义为等同于rgb(0 0 0 / 0%),这是其计算值。这个关键字通常不用于设置文本颜色,但它是元素背景颜色的默认值。它还可以用于定义占据空间但不可见的元素边框,并且在定义渐变时经常使用——这些都是我们后续章节将涵盖的话题。

相比之下,currentcolor意味着“对于该元素,color的计算值是什么就是什么”。考虑以下情况:

main {color: gray; border-color: currentcolor;}

第一个声明导致任何 <main> 元素具有前景色为 gray。第二个声明使用 currentcolor 复制 color 的计算值——在本例中为 gray——并将其应用于 <main> 元素可能具有的任何边框上。顺便说一句,currentcolor 实际上是 border-color 的默认值,我们将在 第七章 中详细讨论。

与所有命名颜色一样,这些颜色名称不区分大小写。我们以混合大小写显示 currentcolor 是因为通常为了可读性而这样写。

幸运的是,CSS 有更详细和精确的方法来指定颜色。优点是,通过这些方法,您可以指定颜色谱中的任何颜色,而不仅仅是有限的命名颜色列表。

RGB 和 RGBa 颜色

计算机通过组合不同级别的红、绿和蓝三原色来创建颜色,这种组合通常称为 RGB 颜色。因此,您应该能够在 CSS 中指定这些主要颜色的自定义组合是有道理的。这个解决方案有点复杂,但是可能的,并且回报是值得的,因为 CSS 对您可以生成的颜色几乎没有限制。您可以以四种方式以此方式生成颜色,本节详细介绍。

功能性 RGB 颜色

两种颜色值类型使用 功能性 RGB 标记 而不是十六进制标记。这种类型的颜色值的通用语法是 rgb(*`color`*),其中 color 是用百分比或数字表示的三元组。百分比值可以在 0%100% 范围内,整数可以在 0255 范围内。

因此,使用百分比表示,分别指定白色和黑色的值如下:

rgb(100%,100%,100%)
rgb(0%,0%,0%)

使用整数三元组表示法,相同的颜色将表示如下:

rgb(255,255,255)
rgb(0,0,0)

要记住的一点是,您不能在同一个颜色值中混合整数和百分比。因此,rgb(255,66.67%,50%) 将是无效的,并因此被忽略。

注意

在较新的浏览器中,RGB 值中的逗号可以用简单的空格替换。因此,黑色可以表示为 rgb(0 0 0)rgb(0% 0% 0%)。这对于本章中允许逗号的所有颜色值都是正确的。请注意,一些较新的颜色函数不允许逗号。

假设您希望 <h1> 元素的颜色介于红色和褐红色之间。red 值相当于 rgb(100%,0%,0%),而 maroon 相当于 (50%,0%,0%)。为了得到介于这两者之间的颜色,您可以尝试这样做:

h1 {color: rgb(75%,0%,0%);}

这使得颜色的红色分量比 maroon 更浅,但比 red 更深。另一方面,如果您想创建一种淡红色,您可以提高绿色和蓝色水平:

h1 {color: rgb(75%,50%,50%);}

使用整数三元组表示的最接近等效颜色如下所示:

h1 {color: rgb(191,127,127);}

可视化这些值如何对应颜色的最简单方法是创建一个灰度值表。结果显示在图 5-10 中:

p.one {color: rgb(0%,0%,0%);}
p.two {color: rgb(20%,20%,20%);}
p.three {color: rgb(40%,40%,40%);}
p.four {color: rgb(60%,60%,60%);}
p.five {color: rgb(80%,80%,80%);}
p.six {color: rgb(0,0,0);}
p.seven {color: rgb(51,51,51);}
p.eight {color: rgb(102,102,102);}
p.nine {color: rgb(153,153,153);}
p.ten {color: rgb(204,204,204);}

css5 0510

图 5-10. 设置为灰色调的文本

由于我们正在处理灰度,每个语句中的三个 RGB 值都相同。如果有任何一个与其他不同,就会开始出现颜色色调。例如,如果将rgb(50%,50%,50%)修改为rgb(50%,50%,60%),则结果将是略带蓝色的中等暗色。

您可以在百分比表示法中使用分数。出于某种原因,您可能希望指定颜色为确切的 25.5%红色,40%绿色和 98.6%蓝色:

h2 {color: rgb(25.5%,40%,98.6%);}

超出每个表示法允许范围的值将被剪辑到最接近的范围边缘,这意味着大于100%或小于0%的值将默认为这些允许的极端值。因此,下列声明将被视为注释中指示的值:

P.one {color: rgb(300%,4200%,110%);}   /*  100%,100%,100%  */
P.two {color: rgb(0%,-40%,-5000%);}   /*  0%,0%,0%  */
p.three {color: rgb(42,444,-13);}    /* 42,255,0  */

百分比和整数之间的转换可能看起来是随意的,但没有必要猜测您想要的整数-有一个简单的公式可以计算它们。如果您知道每个 RGB 级别的百分比,只需将它们应用于数字 255,即可得到结果值。假设您有一个颜色为 25%红色,37.5%绿色和 60%蓝色。将这些百分比乘以 255,您将得到 63.75、95.625 和 153。将这些值四舍五入到最接近的整数,就得到rgb(64,96,153)

如果您已经知道百分比值,将其转换为整数并没有多大意义。整数表示法对于使用像 Adobe Photoshop 这样的程序可以在信息对话框中显示整数值的人,或者对于那些对颜色生成技术细节非常熟悉的人更有用,他们通常考虑 0-255 的值。

RGBa 颜色

RGB 表示法可以包括第四个参数定义 alpha 透明值。通过在 RGB 三元组末尾添加 alpha 值,rgb()接受红绿蓝-Alpha 或 RGBa 值,其中 alpha 值是不透明度的度量。

虽然rgb()表示法允许三个或四个值,但在传统的rgba()函数中,Alpha 值必须存在才有效。

例如,假设您希望元素的文本为半透明的白色。这样,文本后面的任何背景色都会“透过”,与半透明的白色混合。您可以写以下两个值之一:

rgb(255 255 255 / 0.5)
rgba(100% 100% 100% / 0.5)  /* commas would also be allowed */

要使颜色完全透明,您将 alpha 值设置为0;要完全不透明,正确的值是1。因此,rgb(0,0,0)rgba(0,0,0,1)将产生完全相同的结果(黑色)。图 5-11 展示了一系列逐渐透明的黑色段落,这是以下规则的结果:

p.one {color: rgb(0,0,0,1);}
p.two {color: rgba(0%,0%,0%,0.8);}
p.three {color: rgb(0 0 0 / 0.6);}
p.four {color: rgba(0% 0% 0% / 0.4);}
p.five {color: rgb(0,0,0,0.2);}

css5 0512

图 5-11. 设置为渐进透明的文本

Alpha 值始终是在01的实数范围内,或者在0%100%的百分比范围内。超出此范围的任何值都将被忽略或重设为最接近的有效 Alpha 值。

十六进制 RGB 颜色

CSS 允许您使用与老式 HTML 网页作者非常熟悉的十六进制颜色表示法定义颜色:

h1 {color: #FF0000;}   /* set H1s to red */
h2 {color: #903BC0;}   /* set H2s to a dusky purple */
h3 {color: #000000;}   /* set H3s to black */
h4 {color: #808080;}   /* set H4s to medium gray */

计算机已经使用十六进制表示法相当长一段时间了,程序员通常要么接受过相关培训,要么通过经验掌握它。他们对十六进制表示法的熟悉度可能导致其在 HTML 中设置颜色时的使用。这一做法延续到了 CSS 中。

它的工作原理是这样的:通过将三个十六进制数在00FF范围内串联起来,你可以设置一种颜色。这种表示法的通用语法是#RRGGBB。注意,这三个数字之间没有空格、逗号或其他分隔符。

十六进制表示法在数学上等效于整数对表示法。例如,rgb(255,255,255)#FFFFFF完全等效,rgb(51,102,128)#336680相同。您可以自由选择您喜欢的表示法,大多数用户代理程序都会以相同的方式呈现它们。如果您有一个可以在十进制和十六进制之间转换的计算器,那么从一种表示法跳转到另一种表示法应该是相当简单的。

对于由三对匹配的数字组成的十六进制数,CSS 允许一种缩写表示法。这种表示法的通用语法是#RGB

h1 {color: #000;}   /* set H1s to black */
h2 {color: #666;}   /* set H2s to dark gray */
h3 {color: #FFF;}   /* set H3s to white */

正如您从标记中可以看到的那样,每个颜色值仅有三个数字。然而,由于十六进制数在00FF之间需要每个两个数字,并且您仅有三个总共的数字,这种方法是如何工作的?

答案是浏览器取每个数字并复制它。因此,#F00等同于#FF0000#6FA将等同于#66FFAA,而#FFF将变成#FFFFFF,与白色相同。并非所有颜色都可以用这种方式表示。例如,中灰色将按标准十六进制表示为#808080。这不能用简写表示;最接近的等价物将是#888,与#888888相同。

十六进制 RGBA 颜色

十六进制表示法可以有第四个十六进制值来表示 Alpha 通道值。以下规则为设置在图 5-12 中的一系列段落样式,这些段落设置为越来越透明的黑色,就像你在前一节中看到的一样:

p.one {color: #000000FF;}
p.two {color: #000000CC;}
p.three {color: #00000099;}
p.four {color: #00000066;}
p.five {color: #00000033;}

css5 0512

图 5-12。采用渐进透明度设置的文本,重新归纳

与非 Alpha 十六进制值一样,您可以将由匹配对组成的值缩短为四位数值。因此,#663399AA的值可以写为#639A。如果值中有任何不重复的对,整个八位数值必须写出:#663399CA不能缩写为#639CA

HSL 和 HSLa 颜色

色相、饱和度和亮度(HSL)颜色标记类似于色相、饱和度和亮度(HSB),这是图像编辑软件(如 Photoshop)中的颜色系统,同样直观。色相被表达为角度值,饱和度是从 0%(无饱和度)到 100%(完全饱和度)的百分比值,亮度是从 0%(完全暗)到 100%(完全亮)的百分比值。如果你非常熟悉 RGB,刚开始时可能会觉得 HSL 有些困惑。(但是,对于熟悉 HSL 的人来说,RGB 也是困惑的。)

色相角度是围绕一个圆圈表达的,沿着整个颜色光谱进行。从 0 度的红色开始,然后通过彩虹进行,直到在 360 度处再次到达红色。当色相值是一个无单位的数字时,它被解释为度数。

饱和度衡量颜色的强度。无论设置了什么色相角度,0%的饱和度总是产生灰色阴影,而100%的饱和度则创建给定亮度下(在 HSL 颜色空间中)最生动的那种色相阴影。

同样地,亮度定义了颜色看起来是多么暗或亮。无论设置了什么色相和饱和度值,0%的亮度总是黑色,而100%的亮度总是白色。考虑以下样式的结果,这在图 5-13 的左侧有所说明。

p.one {color: hsl(0,0%,0%);}
p.two{color: hsl(60 0% 25%);}
p.three {color: hsl(120deg,0%,50%);}
p.four {color: hsl(180deg 0% 75%);}
p.five {color: hsl(0.667turn,0%,0%);}
p.six {color: hsl(0.833turn 0% 25%);}
p.seven {color: hsl(400grad 0% 50%);}
注意

请记住,在较新的浏览器中,hsl()值中的逗号可以用空格替换。

你在左侧看到的灰色并不仅仅是印刷的限制函数:每一个段落都是灰色的,因为每个颜色值在饱和度(中间)位置上都是0%。亮度或暗度由亮度(第三)位置设置。在所有七个例子中,色相角度都会改变,但都不会影响。

css5 0513

图 5-13. 变化的亮度和色相

但这仅在饱和度保持在0%的情况下成立。如果该值提高到,比如说,50%,那么色相角度将变得非常重要,因为它将控制你看到的颜色类型。考虑之前看到的同一组值,但所有设定为50%饱和度;这在图 5-13 的右侧有所说明,尽管这本书的印刷版本中颜色不可见。

就像 RGB 有一个传统的 RGBA 对应物一样,HSL 也有一个 HSLA 对应物。这是一个 HSL 三元组,后面跟着一个范围在 0 到 1 之间的 alpha 值。以下的 HSLA 值都是黑色,具有不同程度的透明度,就像在“十六进制 RGBA 颜色”(并且在图 5-12 中说明)中所示:

p.one {color: hsl(0,0%,0%,1);}
p.two {color: hsla(0,0%,0%,0.8);}
p.three {color: hsl(0 0% 0% / 0.6);}
p.four {color: hsla(0 0% 0% / 0.4);}
p.five {color: hsl(0rad 0% 0% / 0.2);}

带有 HWB 的颜色

颜色也可以通过使用hwb()功能值来表示其色调白色级别和黑色级别。该函数值接受色调值表示为角度值。在色调角度之后,不是亮度和饱和度,而是白度和黑度值被指定为百分比。

与 HSL 不同的是,hwba()函数没有传统的遗留。相反,hwb()的值语法允许在 HWB 值后面定义一个不透明度,用斜杠(/)与它们分隔开。不透明度可以用百分比或从 0 到 1(包括)的实数值表示。与 HSL 不同的是,逗号是不支持的:HWB 值只能用空格分隔。

这里是使用 HWB 表示法的一些例子:

/* Varying shades of red */
hwb(0 40% 20%)
hwb(360 50% 10%)
hwb(0deg 10% 10%)
hwb(0rad 60% 0%)
hwb(0turn 0% 40%)

/* Partially translucent red */
hwb(0 10% 10% / 0.4)
hwb(0 10% 10% / 40%)

Lab 颜色

历史上,所有的 CSS 颜色都是在 sRGB 颜色空间中定义的,这个空间包含比旧显示器能表示的颜色更多的颜色。然而,现代显示器可以处理大约 sRGB 颜色空间的 150%,这仍然不是人类可以感知到的颜色范围的全部,但已经非常接近了。

1931 年,国际照明委员会(Commission Internationale de l’Éclairage,简称 CIE)定义了一个科学系统,用于定义通过光创建的颜色,而不是通过油漆或染料创建的颜色。现在,将近一个世纪后,CSS 已经将 CIE 的工作引入了其技术手段。

它使用lab()函数值来在 CIE Lab(以下简称为 Lab)颜色空间中表示颜色。Lab 设计用于表示人类可以看到的整个颜色范围。lab()函数接受三到四个参数:lab(L* a b / A)。与 HWB 类似,参数必须用空格分隔(不允许逗号),如果提供了 alpha 值,则斜杠(/)在其前面。

L(亮度)分量指定了 CIE 亮度,并且是从0%表示黑色到100%表示白色的百分比,或者从01数字。第二个分量,a,是 Lab 颜色空间中沿 a 轴的距离。该轴从正方向的紫红色到负方向的绿色阴影。第三个分量,b,是 Lab 颜色空间中沿 b 轴的距离。该轴从正方向的黄色到负方向的蓝紫色。

第四个可选参数是不透明度,其值从 0 到 1(包括),或者从 0%到 100%(包括)。如果省略,则不透明度默认为 1(100%),即完全不透明。

这里是一些在 CSS 中表示 Lab 颜色的例子:

lab(29.2345% 39.3825 -20.0664);
lab(52.2345% 40.1645 59.9971);
lab(52.2345% 40.1645 59.9971 / .5);

将 Lab(以及稍后我们将讨论的 LCH)颜色引入 CSS 的主要原因是它们被系统设计为 感知均匀:具有相同坐标的颜色在该坐标方面看起来是一致的。两个具有不同色调但相同亮度的颜色将看起来具有相似的亮度。具有相同色调但不同亮度的两个颜色将看起来是单一色调的不同阴影。而这在 RGB 和 HSL 值中通常不是这样,因此 Lab 和 LCH 代表了一个重大改进。

它们还被定义为设备独立的,因此您应该能够在这些颜色空间中指定颜色,并从一个设备到另一个设备获得视觉上一致的结果。

警告

截至 2022 年底,只有 WebKit 支持 lab()

LCH 颜色

亮度色度色调(LCH) 是设计来表示人类视觉整个光谱的 Lab 的一个版本。它使用不同的符号表示:lch(L C H / A)。其主要区别在于 CH 是极坐标,而不是沿着颜色轴的线性值。

L(亮度)组件与 CIE 亮度相同,是从 0% 表示黑色到 100% 表示白色的

C(色度量)组件大致表示颜色的数量。其最小值为 0,没有定义最大值。负值的 C 被夹紧到 0

H(色调角度)组件本质上是 lab()ab 值的组合。值 0 沿着正向 a 轴(朝紫红色);90 沿着正向 b 轴(朝芥末黄);180 沿着负向 a 轴(朝青绿色);270 沿着负向 b 轴(朝天蓝色)。此组件大致对应于 HSL 的色调,但色调角度不同。

可选的 A(alpha)组件可以是从 0 到 1 的 <number>,或者是 <percentage>,其中数字 1 对应于 100%(完全不透明)。如果存在,它前面会有一个斜杠(/)。以下是一些示例:

lch(56% 132 331)
lch(52% 132 8)
lch(52% 132 8 / 50%)

为了举例说明 LCH 的能力,lch(52% 132 8) 是非常明亮的品红色,相当于 rgb(118.23% -46.78% 40.48%)。注意到大的红色值和负的绿色值,这使得颜色超出了 sRGB 色彩空间。如果您将该 RGB 值提供给浏览器,它会将该值夹紧到 rgb(100% 0% 40.48%)。这是在 sRGB 色彩空间内的,但在视觉上与 lch(52% 132 8) 定义的颜色非常不同。

警告

截至 2022 年底,只有 Safari 支持 lch() 值。

Oklab 和 Oklch

改进的 Lab 和 LCH 版本,称为OklabOklch,将通过oklab()oklch()功能值在 CSS 中得到支持。Oklab 通过对大量视觉上相似的颜色进行数值优化开发而来,提供了比 CIE 颜色空间更好的色调线性和统一性,以及更好的色度统一性。Oklch 是 Oklab 的极坐标版本,就像 LCH 是 Lab 的一样。

由于这种改进的统一性,Oklab 和 Oklch 将成为 CSS 中颜色插值计算的默认值。然而,截至 2022 年末,只有 Safari 支持oklab()oklch() CSS 功能值。

使用color()

color()函数值允许在命名的颜色空间中指定颜色,而不是隐式的 sRGB 颜色空间。它接受四个以空格分隔的参数,以及一个可选的第五个不透明度值,该值前面带有斜线/

第一个参数是预定义的命名颜色空间。截至 2022 年末,可能的值包括srgbsrgb-lineardisplay-p3a98-rgbprophoto-rgbrec2020xyzxyz-d50xyz-d65。其后的三个值特定于第一个参数声明的颜色空间。某些颜色空间可能允许这些值为百分比,而其他可能不允许。

举个例子,以下数值应该产生相同的颜色:

#7654CD
rgb(46.27% 32.94% 80.39%)
lab(44.36% 36.05 -58.99)
color(xyz-d50 0.2005 0.14089 0.4472)
color(xyz-d65 0.21661 0.14602 0.59452)

您可以轻松地声明一个超出给定颜色空间色域的颜色。例如,color(display-p3 -0.6112 1.0079 -0.2192);超出了display-p3色域。这仍然是一个有效的颜色,只是不能用该颜色空间来表示。当一个颜色值是有效的但超出色域时,它将被映射到该色空间色域内最接近的颜色。

如果一个颜色值是完全无效的,不透明黑色将被使用。

警告

截至 2022 年末,只有 Safari 支持color()

应用颜色

由于我们刚刚讨论了所有可能的颜色格式,让我们简要地谈一下最常使用颜色值的属性:color。这个属性设置元素文本的颜色和currentcolor的值。

此属性接受任何有效的颜色类型作为值,例如#FFCC00rgb(100% 80% 0% / 0.5)

对于像段落或<em>元素这样的不可替代元素,color设置元素中文本的颜色。以下代码将产生图 5-14:

<p style="color: gray;">This paragraph has a gray foreground.</p>
<p>This paragraph has the default foreground.</p>

css5 0514

图 5-14. 声明的颜色与默认颜色

在这个例子中,默认的前景色是黑色。这不一定是这样,因为用户可能已经设置他们的浏览器(或其他用户代理程序)来使用不同的前景(文本)颜色。如果浏览器的默认文本颜色设置为green,则前面例子中的第二段落将是绿色,而不是黑色,但第一段落仍将是灰色。

你不必局限于这些基本操作。有许多方法可以使用 color。你可能有一些段落包含警告用户潜在问题的文本。为了使这些文本比平常更加突出,你可能决定将其着色为红色。只需将 warn 类应用于包含警告文本的每个段落(<p class="warn">),以及以下规则:

p.warn {color: red;}

在同一文档中,你可能决定警告段落中的任何未访问超链接都应为绿色:

p.warn {color: red;}
p.warn a:link {color: green;}

然后你改变主意,决定警告文本应为深红色,并且此类文本中的未访问链接应为中紫色。只需更改前面的规则以反映新值。以下代码导致 图 5-15:

p.warn {color: #600;}
p.warn a:link {color: #400040;}

css5 0515

图 5-15. 改变颜色

color 的另一个用途是吸引注意力到某些类型的文本上。例如,加粗文本已经相当明显了,但你可以给它们赋予不同的颜色以进一步突出 —— 比如说,栗色:

b, strong {color: maroon;}

然后你决定希望所有具有 highlight 类的表格单元格包含浅黄色文本:

td.highlight {color: #FF9;}

如果你没有为任何文本设置背景颜色,你可能会面临用户的设置与你自己的设置不匹配的风险。例如,如果用户将其浏览器的背景设置为淡黄色,如 #FFC,那么上述规则将生成浅黄色文本在淡黄色背景上。更可能的情况是,默认背景仍然是白色,浅黄色文本在白色背景上仍然很难阅读。因此,通常最好同时设置前景色和背景色(我们将很快讨论背景色)。

影响表单元素

设置 color 的值应该(理论上来说)适用于表单元素。声明 <select> 元素具有深灰色文本应该像这样简单:

select {color: rgb(33%,33%,33%);}

这也可能会设置 <select> 元素边缘周围的边框颜色,也可能不会。这完全取决于用户代理及其默认样式。

你也可以设置输入元素的前景色 —— 尽管,正如你在 图 5-16 中所看到的,这样做会将该颜色应用于从文本到单选按钮到复选框输入的所有输入:

select {color: rgb(33%,33%,33%);}
input {color: red;}

css5 0516

图 5-16. 改变表单元素的前景色

注意在 图 5-16 中,复选框旁边的文本颜色仍然是黑色。这是因为所示的规则仅将样式分配给像 <input><select> 这样的元素,而不是普通段落(或其他)文本。

还要注意,复选框中的勾号是黑色的。这是由于某些网页浏览器处理表单元素的方式,它们通常使用基本操作系统内置的表单部件。因此,当您看到复选框和勾号时,它们实际上不是 HTML 文档中的内容,而是已插入文档中的 UI 部件,就像图像一样。事实上,表单输入框像图像一样是替换元素。理论上,CSS 不会样式化表单元素的内容(尽管这可能会在未来发生变化)。

在实践中,这条线比这更模糊,正如图 5-16 所示。一些表单输入框具有其文本甚至 UI 的颜色部分改变,而其他表单输入框则没有。由于规则未明确定义,不同浏览器中的行为是不一致的。简言之,表单元素在样式上非常棘手,应谨慎对待。

继承颜色

正如color的定义所示,该属性是继承的。这是有道理的,因为如果你声明p {color: gray;},你可能期望该段落中的任何文本都是灰色的,即使是强调或加粗的文本也是如此。如果需要使这些元素具有不同的颜色,这也很容易。例如,以下代码将产生图 5-17:

em {color: red;}
p {color: gray;}

css5 0517

图 5-17. 不同元素的不同颜色

由于颜色是继承的,理论上可以通过声明body {color: red;}来将文档中所有普通文本设置为红色。这应该使得所有未经其他样式处理的文本(例如具有自己颜色样式的锚点)都变为红色。

角度

既然我们刚刚谈到了多种颜色值类型中的色相角度,现在是讨论角度单位的好时机。通常情况下,角度被表示为<angle>, 它是一个<number>后跟四种单位类型之一:

deg

度数,一个完整圆圈中有 360 度。

grad

百分度,一个完整圆圈中有 400。也称为gradesgons

rad

弧度,一个完整圆圈中有 2π(约 6.28)。

turn

转角,一个完整圆圈中有一个。这个单位在进行旋转动画时非常有用,例如10turn可以使其旋转 10 次。(遗憾的是,复数形式turns是无效的,至少到 2023 年初为止,将被忽略。)

为了帮助理解这些角度类型之间的关系,表格 5-2 显示了一些角度在不同单位下的表达方式。与长度值不同,包括角度时总是需要单位,即使数值是0deg

表格 5-2. 角度等效

百分度 弧度 转角
0deg 0grad 0rad 0turn
45deg 50grad 0.785rad 0.125turn
90deg 100grad 1.571rad 0.25turn
180deg 200grad 3.142rad 0.5turn
270deg 300grad 4.712rad 0.75turn
360deg 400grad 6.283rad 1turn

时间和频率

当属性需要表达时间段时,该值表示为<time>,是一个<number>后跟s(秒)或ms(毫秒)。时间值通常用于过渡和动画中,用于定义持续时间或延迟。以下两个声明将完全产生相同的结果:

a[href] {transition-duration: 2.4s;}
a[href] {transition-duration: 2400ms;}

时间值也用于声音 CSS 中,用于定义持续时间或延迟,但截至目前,对声音 CSS 的支持非常有限。

声音 CSS 中历史上使用的另一种值类型是<frequency>,它是一个<number>后跟Hz(赫兹)或kHz(千赫)。与往常一样,单位标识符大小写不敏感,所以Hzhz是等效的。以下两个声明将完全产生相同的结果:

h1 {pitch: 128hz;}
h1 {pitch: 0.128khz;}

与长度值不同,对于时间和频率值,单位类型始终是必需的,即使值为0s0hz

比率

当你需要表示两个数字的比率时,你使用一个<ratio>值。这些值表示为两个正<number>值,它们用斜杠(/)分隔,可以带有可选的空白。

第一个整数指的是元素的宽度(内联尺寸),第二个指的是高度(块尺寸)。因此,要表示高宽比为 16 比 9,你可以写16/916 / 9

截至 2022 年末,还没有方法将比率表达为单个实数(例如,1.777代替16/9),也没有使用冒号分隔符而不是斜杠(例如,16:9)的设施。

位置

你使用一个位置值,表示为<position>,来指定背景区域中原始图像的放置位置。它的语法结构相当复杂:

[
  [ left | center | right | top | bottom | *<percentage>* | *<length>* ] |
  [ left | center | right | *<percentage>* | *<length>* ]
  [ top | center | bottom | *<percentage>* | *<length>* ] |
  [ center | [ left | right ] [ *<percentage>* | *<length>* ]? ] &&
  [ center | [ top | bottom ] [ *<percentage>* | *<length>* ]? ]
]

这可能看起来有点疯狂,但这一值类型要允许的微妙复杂模式就是这样。

如果只声明一个值,如left25%,第二个值被设为center。因此,left等同于left center25%等同于25% center

如果声明两个值(无论是隐式地,如前面的示例中,还是显式地),并且第一个是<length>或<percentage>,则始终视为水平值。因此,给定25% 35px25%是水平距离,35px是垂直距离。如果你交换它们变成35px 25%,那么35px是水平的,25%是垂直的。如果你写25% left35px right,整个值都是无效的,因为你提供了两个水平距离而没有垂直距离。(类似地,right lefttop bottom的值是无效的并将被忽略。)另一方面,如果你写left 25%right 35px,那就没有问题,因为你给出了一个水平距离(用关键字)和一个垂直距离(用百分比或长度)。

如果声明了四个值(我们稍后处理三个),必须有两个长度或百分比,每个之前都有一个关键字。在这种情况下,每个长度或百分比指定一个偏移距离,每个关键字定义了偏移量计算的边缘。因此,right 10px bottom 30px意味着距离右边缘左侧 10 像素,距离底边缘上方 30 像素的偏移量。类似地,top 50% left 35px意味着距离顶部 50%的偏移量和距离左边缘右侧 35 像素的偏移量。

使用background-position属性时,只能声明三个位置值。如果声明了三个值,则规则与四个值相同,只是第四个偏移量被设置为 0(无偏移)。因此,right 20px topright 20px top 0相同。

自定义属性

如果你使用过 Less 或 Sass 等预处理器,可能已经创建了用于保存值的变量。CSS 本身也具有这种能力。这个技术术语称为自定义属性,尽管它们实际上创建的是类似于 CSS 中的变量。

这是一个基本的示例,结果显示在图 5-18 中(尽管颜色在打印版本中看不到)。

html {
    --base-color: #639;
    --highlight-color: #AEA;
}

h1 {color: var(--base-color);}
h2 {color: var(--highlight-color);}

css5 0518

图 5-18. 使用自定义值给标题上色

这里有两件事需要吸收。第一是自定义值--base-color--highlight-color的定义。这些并不是某种特殊的颜色类型。它们只是我们选定的名称,用来描述值的内容。我们也可以说成这样:

html {
    --alison: #639;
    --david: #AEA;
}

h1 {color: var(--alison);}
h2 {color: var(--david);}

你可能不应该做这种事情,除非你确实在定义与名为 Alison 和 David 的人名特定对应的颜色(也许在“关于我们的团队”页面上)。最好定义自文档化的自定义标识符,如main-coloraccent-colorbrand-font-face

重要的是,这类自定义标识符始于两个连字符(--)。稍后可以通过var()值类型调用它们。请注意,这些名称区分大小写,因此--main-color--Main-color是完全独立的标识符。

这些自定义标识符通常称为CSS 变量,这解释了var()模式。自定义属性的一个有趣特性是能够将自身限定到 DOM 的特定部分。如果这句话让你有点明白,那可能会让你感到一丝兴奋。如果不明白,下面有一个示例来说明作用域,结果显示在图 5-19 中:

html {
    --base-color: #666;
}
aside {
    --base-color: #CCC;
}

h1 {color: var(--base-color);}
<body>

<h1>Heading 1</h1><p>Main text.</p>

<aside>
    <h1>Heading 1</h1><p>An aside.</p>
</aside>

<h1>Heading 1</h1><p>Main text.</p>

</body>

css5 0519

图 5-19. 将自定义值限定到特定上下文

请注意,标题在<aside>元素外部是深灰色,在内部是浅灰色。这是因为--base-color变量已经更新为<aside>元素的标题。新的自定义值适用于<aside>元素内的任何<h1>

即使限制为值替换,CSS 变量也可以产生很多模式。这里是由 Chriztian Steinmeier 提出的一个示例,将变量与 calc() 函数结合起来创建无序列表的常规缩进集合:

html {
    --gutter: 3ch;
    --offset: 1;
}
ul li {margin-left: calc(var(--gutter) * var(--offset));}
ul ul li {--offset: 2;}
ul ul ul li {--offset: 3;}

这个特定的例子基本上和写下面这个是一样的:

ul li {margin-left: 3ch;}
ul ul li {margin-left: 6ch;}
ul ul ul li {margin-left: 9ch;}

不同之处在于,使用变量时,可以在一个地方简单地更新 --gutter 的乘数,并使所有内容自动调整,而不必重新输入三个值并确保所有数学计算都是正确的。

自定义属性回退

当你使用 var() 设置一个值时,你可以指定一个回退值。例如,你可以说,如果一个自定义属性没有定义,你希望使用一个常规值,如下所示:

ol li {margin-left: var(--list-indent, 2em);}

鉴于此,如果 --list-indent 没有定义,被确定为无效,或者被明确设置为 initial,将使用 2em 代替。你只会得到一个回退值,并且不能是另一个自定义属性名称。

也就是说,它 可以 是另一个 var() 表达式,而且嵌套的 var() 可能包含另一个 var() 作为其回退,依此类推。所以,假设你使用了一个定义了各种界面元素颜色的模式库。如果由于某种原因这些颜色不可用,你可以回退到基本站点样式表定义的自定义属性值。然后,如果这个值也不可用,你可以回退到一个普通的颜色值。它看起来像这样:

.popup {color: var(--pattern-modal-color, var(--highlight-color, maroon));}

这里需要注意的是,如果你设法创建了一个无效的值,整个东西都会被炸掉,值要么被继承,要么被设置为它的初始值,具体取决于属性通常是继承的还是不继承的,就像被设置为 unset 一样(参见 unset)。

假设我们写了以下无效的 var() 值:

:root {
	--list-color: hsl(23, 25%, 50%);
	--list-indent: 5vw;
}

li {
	color: var(--list-color, --base-color, gray);
	margin-left: var(--list-indent, --left-indent, 2em);
}

在第一种情况下,回退是 --base-color, gray 作为单个字符串,而不是一个被解析的东西,因此是无效的。类似地,在第二种情况下,回退 --left-indent 从未声明过。无论哪种情况,如果第一个自定义属性有效,无效的回退就无关紧要,因为浏览器永远不会到达它。但是,如果,比如说,--list-indent 没有值,浏览器将会转向回退,这里是无效的。那么接下来会发生什么呢?

对于颜色,因为属性 color 是继承的,列表项将从它们的父元素(几乎肯定是 <ol><ul> 元素)继承它们的颜色。如果父元素的 color 值是 fuchsia,那么列表项将是紫红色的。对于左边距,属性 margin-left 不是继承的,所以列表项的左边距将被设置为 margin-left 的初始值,即 0。因此,列表项将没有左边距。

如果你试图将一个不能接受这些类型值的值应用于一个属性,也会发生这种情况。考虑以下例子:

:root {
	--list-color: hsl(23, 25%, 50%);
	--list-indent: 5vw;
}

li {
	color: var(--list-indent, gray);
	margin-left: var(--list-color, 2em);
}

在这里,乍看之下一切看起来都很好,只是 color 属性被赋予了一个长度值,而 margin-left 属性被赋予了一个颜色值。因此,gray2em 的回退值没有被使用。这是因为 var() 语法是有效的,所以结果与我们声明 color: 5vwmargin-left: hsl(23, 25%, 50%) 相同,这两者都被视为无效而被丢弃。

这意味着结果与之前看到的相同:列表项将继承其父元素的颜色值,并且它们的左边距将被设置为初始值 0,就好像给定的值是 unset 一样。

摘要

正如你所见,CSS 提供了各种数值和单位类型。这些单位根据使用的具体情况可能有优势和缺点。你已经看到了一些情况,这些微妙之处将在本书的其余部分中适当地讨论。

第六章:基本视觉格式化

您可能经历过打算的布局未按预期渲染的沮丧。即使添加了 27 条样式规则使其完美,您可能仍然不知道哪条规则解决了您的问题。由于 CSS 中包含的模型如此开放且强大,没有一本书能够涵盖每种组合属性和效果的可能方式。您无疑会继续发现新的 CSS 使用方式。然而,通过彻底掌握视觉渲染模型的工作原理,您将更能够确定某种行为是否是渲染引擎 CSS 定义的正确(尽管意外的)结果。

基本框

在其核心,CSS 假设每个元素生成一个或多个称为元素框的矩形框。(规范的未来版本可能允许非矩形形状,实际上已经提出了更改,但目前所有框仍然是矩形的。)

每个元素框在其中心具有一个内容区域。该内容区域周围可选的填充、边框、轮廓和边距。这些区域被视为可选,因为它们都可以设置为 0 大小,有效地将它们从元素框中移除。图 6-1 展示了一个内容区域示例,以及填充、边框和边距的周围区域。

css5 0601

图 6-1. 内容区域及其周围

在查看可以改变元素占用空间的属性之前,让我们先了解完全理解如何布局元素和占用空间所需的词汇。

快速入门

首先,我们将快速回顾我们将讨论的框类型,以及需要理解接下来的解释所需的一些重要术语:

块流方向

也称为块轴,这是堆叠块级元素框的方向。在包括所有欧洲和欧洲衍生语言在内的许多语言中,这个方向是从上到下的。在中文/日文/韩文(CJK)语言中,这可以是从右到左或从上到下。实际的块流方向由书写模式设置,这在第十五章中讨论。

内联基本方向

也被称为内联轴,这是文本行书写的方向。在罗曼语言等语言中,这是从左到右。在阿拉伯语或希伯来语等语言中,内联基本方向是从右到左。在 CJK 语言中,这可以是从上到下或从左到右。与块流方向一样,内联基本方向由书写模式设置。

正常流

元素在浏览器视口内放置的默认系统,基于父元素的书写模式。大多数元素都处于正常流中,元素离开正常流的唯一方法是浮动、定位或转变为弹性盒、网格布局或表格元素。本章讨论的内容涵盖正常流中的元素,除非另有说明。

块框

这是由段落、标题或 <div> 等元素生成的框盒。这些框在正常流中的前后都生成空白空间,以便块框在块流轴上依次堆叠。几乎任何元素都可以通过声明 display: block 来生成块框,尽管还有其他使元素生成块框的方法(例如浮动它们或使它们成为弹性项目)。

行内框

这是由 <strong><span> 等元素生成的框。这些框沿着行内基本方向布局,不会在其前后生成换行。默认情况下,长度超过元素行内尺寸的行内框(如果它是非替换的)将换行成多行。通过声明 display: inline,任何元素都可以生成行内框。

非替换元素

这是其内容包含在文档中的元素。例如,段落(<p>)是非替换元素,因为其文本内容位于元素本身内部。

替换元素

这是一个用作其他内容占位符的元素。替换元素的经典示例是 <img>,它简单地指向一个图像文件,该文件被插入到文档流中 <img> 元素所在的位置。大多数表单元素也是替换元素(例如 <input type="radio">)。

根元素

这是文档树顶部的元素。在 HTML 文档中,这是 <html> 元素。在 XML 文档中,它可以是语言允许的任何元素:例如,RSS 文件的根元素是 <rss>,而在 SVG 文档中,根元素是 <svg>

包含块

我们需要详细研究另一种盒子,足够详细以至于它值得拥有自己的部分:包含块

每个元素的框盒都是根据其包含块布局的。在实际情况中,包含块是盒子的布局上下文。CSS 定义了一系列规则来确定盒子的包含块。

对于给定的元素,包含块从生成列表项或块框的最近祖先元素的内容边缘形成,其中包括所有与表格相关的框(例如由表格单元格生成的框)。考虑以下内容:

<body>
    <div>
        <p>This is a paragraph.</p>
    </div>
</body>

在这个简单的标记中,<p> 元素块框的包含块是 <div> 元素的块框,因为它是最近的具有块或列表项框的祖先元素框(在这种情况下,它是一个块框)。类似地,<div> 的包含块是 <body> 的框。因此,<p> 的布局取决于 <div> 的布局,而 <div> 的布局又取决于 <body> 元素的布局。

而在文档树中更高的位置,<body> 元素的布局取决于 <html> 元素的布局,其框创建了所谓的初始包含块。这在视口单位中非常重要,视口决定了初始包含块的尺寸,而不是根元素内容的尺寸。这很重要,因为内容可能短于或长于视口的尺寸。大多数情况下这没有影响,但对于固定定位或视口单位等情况,差异是真实的。

现在您理解了一些术语,我们可以讨论构成图 6-1 的属性。各种边距、边框和填充功能,如border-style,可以使用各种特定于边的长手属性进行设置,例如margin-inline-startborder-bottom-width。(轮廓属性没有特定于边的属性;对轮廓属性的更改会影响所有四个边。)

默认情况下,内容的背景(例如颜色或平铺图像)应用于填充和边框区域,但这是可以更改的。边距始终是透明的,允许任何父元素的背景可见。填充和边框不能为负长度,但边距可以为负。我们将在“负边距和折叠”中探讨负边距的效果。

边框通常使用定义的样式生成,例如soliddottedinset作为border-style属性,并使用border-color属性设置其颜色。如果未设置颜色,则默认值为currentcolor。边框也可以由图像生成。如果边框样式具有某种类型的间隙,例如border-style: dashed或从部分透明图像生成的边框,则默认情况下元素的背景可通过这些间隙看到,尽管可以剪切背景以保持在边框(或填充)内。

修改元素显示

通过设置display属性的值,可以影响用户代理程序的显示方式。

我们将忽略 rubytable 相关的值,因为它们对于本章节来说过于复杂。我们也会暂时忽略值 list-item,因为它与块级框非常相似,并且在 第十六章 中有详细探讨。现在,让我们花点时间谈谈如何改变元素的显示角色会改变布局。

更改角色

在样式化文档时,有时候能够改变元素生成的框的类型是很方便的。例如,假设我们有一系列链接在 <nav> 中,我们希望将它们布局为垂直边栏:

<nav>
    <a href="index.html">WidgetCo Home</a>
    <a href="products.html">Products</a>
    <a href="services.html">Services</a>
    <a href="fun.html">Widgety Fun!</a>
    <a href="support.html">Support</a>
    <a href="about.html" id="current">About Us</a>
    <a href="contact.html">Contact</a>
</nav>

默认情况下,链接将生成内联框,并且会被压缩成一个看起来像是仅包含链接的短段落。我们可以将所有链接放入它们自己的段落或列表项中,或者我们可以将它们都转换为块级元素,就像这样:

nav a {display: block;}

这将使导航元素 <nav> 内的每个 <a> 元素生成一个块级框,而不是通常的内联框。如果我们添加一些额外的样式,我们可以得到类似于 图 6-2 所示的结果。

css5 0602

图 6-2. 将显示角色从内联更改为块级

当你希望导航链接在没有 CSS 支持时(可能因为加载失败),将其作为内联元素显示,但在有 CSS 的环境中将它们作为块级元素布局时,更改显示角色会非常有用。你还可以在桌面显示器上将链接显示为内联元素,在移动设备上显示为块级元素,反之亦然。将链接布局为块级元素后,你可以像对待 <div><p> 元素那样对其进行样式设置,整个元素框都成为链接的一部分。

你可能还想将元素变成内联元素。假设我们有一个无序姓名列表:

<ul id="rollcall">
    <li>Bob C.</li>
    <li>Marcio G.</li>
    <li>Eric M.</li>
    <li>Kat M.</li>
    <li>Tristan N.</li>
    <li>Arun R.</li>
    <li>Doron R.</li>
    <li>Susie W.</li>
</ul>

给定这个标记,假设我们希望我们的显示中显示一系列内联姓名,并在它们之间(以及列表的每一端)加上垂直条。唯一的方法是改变它们的显示角色。以下规则将会产生 图 6-3 所示的效果:

#rollcall li {display: inline; border-right: 1px solid; padding: 0 0.33em;}
#rollcall li:first-child {border-left: 1px solid;}

css5 0603

图 6-3. 将显示角色从 list-item 更改为 inline

请理解,大部分情况下,你改变的是元素的显示角色,而不是它们固有的性质。换句话说,使段落生成内联框并不会将该段落转变为内联元素。例如,在 HTML 中,某些元素是块级的,而其他元素是内联的。尽管可以轻松地将 <span> 放置在段落内,但不应该将 <span> 围绕在段落周围。

我们说“大部分情况”,因为虽然 CSS 主要影响呈现而不是内容,但 CSS 属性可能影响辅助功能的方式不仅限于颜色对比。例如,更改display值可能会影响辅助技术对元素的感知方式。将元素的display属性设置为none会从辅助功能树中移除该元素。将 <table>display属性设置为grid可能会导致该表被解释为不同于数据表的东西,从而移除正常的表键盘导航,使得屏幕阅读器用户无法访问该表作为数据表。(这本不应该发生,但在某些浏览器中确实发生了。)

可以通过为表及其所有后代设置可访问的富互联网应用(ARIA)role属性来减轻这种情况。然而,通常情况下,每当您在 CSS 中做出更改强制您在 ARIA 角色中做出更改时,您都应该花时间考虑是否有更好的方法来实现您的目标。

处理块框

块框在可预测的,但有时令人惊讶的方式下运作。例如,沿着块和内联轴的框放置处理可以有所不同。要完全理解块框的处理方式,您必须清楚理解这些框的几个方面。这些详细显示在 图 6-4,展示了在两种不同书写模式下的放置方式。

css5 0604

图 6-4. 两种不同书写模式下的完整框模型

如 图 6-4 所示,CSS 处理块方向和内联方向,以及块大小和内联大小。块大小和内联大小是描述内容区域(默认情况下)在块和内联轴上的大小的描述。

相比之下,宽度(有时称为物理宽度)是指在水平轴(从左到右)上内容区域内部边缘之间的距离,不考虑书写方向,而高度物理高度)是垂直轴(从上到下)上的距离。我们很快会讨论可以设置所有这些尺寸的属性。

在 图 6-4 中需要注意的重要事项是使用 startend 来描述元素框的各个部分。例如,您将看到块开始边距和块结束边距。开始边 是您沿轴移动时首先遇到的边缘。

如果你查看图 6-5,沿着每个轴从箭头尾部到箭头尖端追踪,这可能更加清晰。沿着块轴移动时,你遇到的每个元素的第一个边缘是该元素的块起始边缘。当你离开元素时,你穿过块结束边缘。同样,当你沿着行内轴移动时,你经过行内起始边缘,横穿内容的行内维度,然后穿过行内结束边缘。尝试对三个示例分别进行操作。

css5 0605

图 6-5。三种常见书写模式的块轴和行内轴方向

逻辑元素大小

因为 CSS 识别元素的块轴和行内轴,它提供了属性,让你能够沿着每个轴设置显式的元素大小。

这些属性允许你沿着块轴设置元素的大小,或者沿着行内轴约束文本行的长度,不管文本流的方向如何。如果你写block-size: 500px,元素的块大小将是 500 像素宽,即使这导致内容溢出元素框。我们将在本章节的后面更详细地讨论这一点。

考虑以下内容,在应用于各种书写模式时,其结果显示在图 6-6 中:

p {inline-size: 25ch;}

css5 0606

图 6-6。沿着其行内轴调整元素大小

如图 6-6 所示,元素沿其行内轴一致调整大小,不论书写方向如何。如果你侧着头看,你会发现行在完全相同的地方换行。这在所有书写模式中保持了一致的行长。

类似地,你可以为元素设置一个块大小。这在替换元素如图像中更常用一些,但在任何有意义的情况下都可以使用。举个例子:

p img {block-size: 1.5em;}

完成这些后,任何 <img> 元素在 <p> 元素内部被发现时,其块大小将设置为周围文本大小的 1.5 倍。这适用于图像,因为它们是行内替换元素;对于行内非替换元素,这种方法是行不通的。你也可以使用 block-size 来约束网格布局项的块长度为最小或最大尺寸,例如这样:

#maingrid > nav {block-size: clamp(2rem, 4em, 25vh);}

需要说明的是,通常块大小是自动确定的,因为正常流中的元素通常没有显式设置的块大小。例如,如果一个元素的块流是从上到下,有 8 行,每行高 1/8 英寸,则其块大小将为 1 英寸。如果是 10 行高,则块大小将是 1.25 英寸。在任何情况下,只要block-sizeauto,块大小就由元素的内容确定,而不是由作者确定。这通常是您想要的,特别是对于包含文本的元素。当显式设置block-size并且没有足够的内容填充框时,框内会出现空白空间;如果内容超过框的容纳能力,则内容可能会溢出框或出现滚动条。

基于内容的尺寸值

除了您之前在设置块和内联大小中看到的长度和百分比外,还有一些关键字提供基于内容的大小调整:

max-content

尽可能占据最大的空间以适应内容,甚至在文本内容的情况下抑制换行。

min-content

尽可能占据最少的空间以适应内容。

fit-content

占据由计算max-contentmin-content和常规内容大小值确定的空间量,取min-content和常规大小的最大值,然后取max-content和先前两者中较大值的最小值。是的,这听起来有点混乱,但我们稍后会解释清楚。

如果您曾经使用过 CSS Grid(在第十二章介绍),您可能会认出这些关键字,因为它们最初被定义为调整网格项大小的一种方式。现在它们正开始应用到 CSS 的其他领域。让我们来考虑前两个关键字,它们在图 6-7 中有演示。

css5 0607

图 6-7. 内容大小

左侧的段落设置为max-content,效果如下:段落的宽度会根据需要调整以适应所有内容。之所以宽度如此狭窄,仅因内容不多。如果再添加三句话,单行文本将继续延伸,甚至可能一直延伸到页面之外(或浏览器窗口之外),而不会换行。

在右侧的段落中,内容尽可能窄,而不会在单词内部强制换行或使用连字符。在这种特定情况下,元素的宽度刚刚足够容纳“段落”这个最长的单词。在示例中的每一行文本中,浏览器会尽可能多地放置适合“段落”所需空间的单词,并在空间不足时换行。如果我们在文本中加入“反对清教会主义”,元素的宽度将刚好足够容纳那个单词,而且每行文本很可能包含多个单词。

注意,在图 6-7 的min-content示例结束时,浏览器利用了min-content中存在连字符的情况,触发了一次换行。如果没有做出这个选择,min-content几乎肯定会成为段落中最长的内容片段,并且元素的宽度将设置为该长度。这意味着,如果您的内容包含浏览器认为是自然换行点的符号(例如空格和连字符),它们很可能会被考虑在min-content计算中。如果您希望进一步缩小元素的宽度,可以使用hyphens属性启用单词的自动连字符(参见第十五章)。

想了解更多关于min-content大小的示例,请参见图 6-8。

css5 0608

图 6-8. 最小内容大小

第三个关键词fit-content很有趣,因为它尽力使元素适应内容。实际上,这意味着如果您只有少量内容,元素的内联尺寸(通常是宽度)将足够大以包含它,就像使用max-content一样。如果有足够多的内容以至于可以换行到多行或以其他方式威胁到溢出元素的容器,则内联尺寸就会停在那里。这在图 6-9 中有所说明。

css5 0609

图 6-9. 适应内容大小

在每种情况下,元素都适应内容而不会溢出元素的容器。至少在正常流中的元素是这样。在 flexbox 和 grid 上下文中,行为可能会有所不同,并且在后续章节中进一步探讨。

最小和最大逻辑大小

如果您希望为块或行内大小设置最小和最大限制,CSS 有一些属性可以帮助您。

当您知道要为元素框的大小设置上限和下限,并且愿意允许浏览器在遵守这些限制的同时执行任何操作时,这些属性可能非常有用。例如,您可以像这样设置布局的一部分:

main {min-inline-size: min-content; max-inline-size: 75ch;}

这保持了<main>元素不比行内内容的最宽部分更窄,无论是长单词、插图还是其他内容。它还保持了<main>元素的宽度不超过约 75 个字符,从而保持行长度可读。

还可以设置块大小的限制。一个很好的例子是将正常流中嵌入的任何图像限制为其固有大小,直到某个点为止。以下 CSS 将产生图 6-10 中显示的效果:

#cb1 img {max-block-size: 2em;}
#cb2 img {max-block-size: 1em;}

css5 0610

图 6-10. 最大块大小

高度和宽度

如果你已经使用 CSS 一段时间或在维护遗留代码,你可能习惯于思考“上边距”和“下边距”。这是因为最初,所有框模型方面都是以其物理方向描述的:上、右、下和左。你仍然可以使用物理方向!CSS 只是在混合中添加了新的方向。

如果你在前面的代码示例中将inline-size改为width,你会得到一个更接近于图 6-11 所示的结果(其中垂直书写模式会被截短至其完整高度的一小部分)。

css5 0611

图 6-11. 调整元素宽度

这些元素在水平方向上都被制作为40ch宽,不管其书写方式如何。每个元素的高度已根据内容、书写模式的具体情况等自动确定。

提示

如果你使用类似block-size这样的块和行内属性而不是类似height这样的物理方向,并且你的设计应用到翻译成其他语言的内容上,布局将会自动调整到你的意图。

heightwidth属性被称为物理属性。它们指的是物理方向,与依赖书写的块大小和行内大小相对。因此,height确实指的是从元素内边缘顶部到底部的距离,而不管块轴的方向如何。

在具有水平行内轴(如英语或阿拉伯语)的书写中,如果在同一元素上同时设置了inline-sizewidth,则后声明的属性会覆盖首次声明的属性。如果同时声明了block-sizeheight,并且起源、层和特异性都相同,则后声明的属性会优先。在垂直书写模式中,inline-size对应于heightblock-size对应于width

将块框的高度或宽度设置为<length>意味着它将是那个长度的高或宽,不管其内部内容如何。如果你将生成块框的元素设置为width: 200px,那么它将是 200 像素宽,即使其内部有一个 500 像素宽的图像。

width的值设置为<percentage>意味着元素的宽度将是其包含块宽度的那个百分比。如果你将段落设置为width: 50%,其包含块宽度为 1,024 像素,则段落的width将计算为 512 像素。

height设置为<percentage>的工作方式类似,除非包含块已显式设置高度。如果包含块的高度是自动设置的,则百分比值会被视为auto,如图 6-12 中#cb4示例所示。

注意

对于定位元素、弹性盒和网格元素,auto上下边距的处理方式是不同的。这些差异在第十一章和第十二章中有详细介绍。

这里是一些这些值和组合的示例,结果显示在图 6-12 中:

[id^="cb"] {border: 1px solid;}  /* "cb" for "containing block" */
#cb1 {width: auto;}    #cb1 p {width: auto;}
#cb2 {width: 400px;}   #cb2 p {width: 300px;}
#cb3 {width: 400px;}   #cb3 p {width: 50%;}

#cb4 {height: auto;}   #cb4 p {height: 50%;}
#cb5 {height: 300px;}  #cb5 p {height: 200px;}
#cb6 {height: 300px;}  #cb6 p {height: 50%;}

css5 0612

图 6-12. 高度和宽度

您还可以使用max-contentmin-contentheight属性,但在自上而下的块流中,两者与height: auto相同。在使用水平块轴的书写模式中,为height设置这些值将产生类似于在垂直块流中为width设置它们的效果。

另外,这些属性不适用于内联非替换元素。例如,如果尝试为正常流中生成内联框的超链接声明heightwidth,符合 CSS 的浏览器必须忽略这些声明。假设以下规则:

a:any-link {color: red; background: silver; height: 15px; width: 60px;}

您最终会得到未访问的红色链接,它们的高度和宽度由链接内容决定,位于银色背景上。这些链接的内容区域不会是 15 像素高和 60 像素宽,因为这些值在应用于内联非替换元素时必须被忽略。另一方面,如果您添加display值,如inline-blockblock,那么heightwidth将设置链接内容区域的高度和宽度。

更改盒子大小

如果使用heightwidth(以及block-sizeinline-size)来描述元素内容区域的尺寸而不是其可见区域似乎有点奇怪,您可以通过使用属性box-sizing使此尺寸更直观。

此属性改变了heightwidthblock-sizeinline-size属性的值如何处理。

如果声明inline-size: 400px并且不声明box-sizing的值,元素的内容区域将在行内方向上为 400 像素,任何填充、边框等都将添加到其中。另一方面,如果声明box-sizing: border-box,元素框将从行内起始边框边缘到行内结束边框边缘为 400 像素;任何行内起始或结束的边框或填充将放置在该距离内,从而缩小内容区域的行内尺寸。这在图 6-13 中有所说明。

css5 0613

图 6-13. box-sizing的效果

换个说法,如果声明width: 400px并且不声明box-sizing的值,元素的内容区域将为 400 像素宽,任何填充、边框等都将添加到其中。另一方面,如果声明box-sizing: border-box,元素框将从左外边框边缘到右外边框边缘为 400 像素;任何左侧或右侧的边框或填充将放置在该距离内,从而缩小内容区域的宽度(同样,如图 6-13 中所示)。

我们在这里谈论 box-sizing 属性,因为它适用于“接受 widthheight 值的所有元素”(因为在逻辑属性普及之前它就被定义了)。通常情况下,这适用于生成块框的元素,尽管它也适用于替换的内联元素,比如图像,以及内联块框。

在逻辑和物理方式中已经确定了如何调整元素的尺寸后,让我们扩展范围,查看影响块大小的所有属性。

块轴属性

总体而言,块轴格式化受到七个相关属性的影响:margin-block-startborder-block-startpadding-block-startheightpadding-block-endborder-block-endmargin-block-end。这些属性在图 7-14 中有详细说明。在第七章中,我们将详细讨论这些属性的所有内容,现在我们将先讨论这些属性的一般原则和行为,然后再查看其值的细节。

必须将块轴的起始和结束填充以及边框设置为特定值,否则它们将默认为宽度为 0,假设未声明边框样式。如果已设置 border-style,则边框的厚度设置为 medium,在所有已知的浏览器中宽度为 3 像素。图 7-14 描述了两种书写模式下的块轴格式化属性,并指示框的哪些部分可能具有 auto 的值,哪些可能没有。

css5 0614

图 6-14. 块轴格式化的七个属性,以及可以设置为 auto 的属性

如果在普通流中的块级框中,margin-block-startmargin-block-end 中的一个设置为 auto,但不是两者同时设置,它们都会计算为 0。遗憾的是,值为 0 会阻止在包含块中轻松地使普通流框在块方向上居中(尽管在 flexbox 或 grid 布局中这样的居中相对直接)。

block-size 属性必须设置为 auto 或某种类型的非负值;它永远不能小于 0

自动块大小

在最简单的情况下,具有 block-size: auto 的普通流块框的高度刚好足以包含其内联内容(包括文本)的行框。如果具有自动块大小的普通流块框只有块级子元素,并且没有块边缘的填充或边框,则从其第一个子元素的边框起始边到其最后一个子元素的边框结束边的距离将是框的块大小。这种情况是因为子元素的边距可以“突出”包含它们的元素,这归功于所谓的边距折叠,我们将在“块轴边距折叠”中讨论。

但是,如果块级元素具有块起始或块结束填充,或块起始和块结束边框,则其块大小将是其第一个子元素的块起始边缘到其最后一个子元素的块结束边缘的距离:

<div style="block-size: auto; background: silver;">
    <p style="margin-block-start: 2em; margin-block-end: 2em;">A paragraph!</p>
</div>
<div style="block-size: auto; border-block-start: 1px solid;
 border-block-end: 1px solid; background: silver;">
    <p style="margin-block-start: 2em; margin-block-end: 2em;">
         Another paragraph!</p>
</div>

图 6-15 展示了这两种行为。

如果我们将前面示例中的边框改为填充,那么 <div> 的块大小效果将相同:它仍将包含段落的边距在内。

css5 0615

图 6-15. 带有块级子元素的自动块大小

百分比高度

您之前看到了如何处理长度值块大小,现在我们花一点时间讨论百分比。如果正常流块框的块大小设置为百分比值,则该值将视为其包含块的块大小的百分比,假设容器具有自己的显式非 auto 块大小。给定以下标记,段落沿块轴的长度为 3 em:

<div style="block-size: 6em;">
    <p style="block-size: 50%;">Half as tall</p>
</div>

如果包含块的块大小 没有 显式声明,则百分比块大小将重置为 auto。如果我们更改上一个示例,以便 <div>block-sizeauto,则段落现在将自动确定其块大小:

<div style="block-size: auto;">
    <p style="block-size: 50%;">NOT half as tall; block size reset to auto</p>
</div>

图 6-16 阐述了这两种可能性。(段落边框与 <div> 边框之间的空间是段落的块起始和结束边距。)

css5 0616

图 6-16. 不同情况下的百分比块大小

在我们继续之前,请仔细观察 图 6-16 中的第一个示例,即半高的段落。它可能只有高度的一半,但沿块轴并非居中。这是因为包含的 <div> 高度为 6 em,这意味着半高的段落为 3 em。由于浏览器的默认样式,它具有 1 em 的块起始和结束边距,因此其整体块大小为 5 em。这意味着段落可见框的块结束边缘与 <div> 的块结束边框之间实际上有 2 em 的空间,而不是 1 em。 图 6-17 详细说明了这一点。

css5 0617

图 6-17. 块轴尺寸和详细放置

处理内容溢出

由于可以将元素设置为特定大小,因此可能使元素的内容无法完全容纳。如果显式定义块大小,则更有可能出现此问题,但在后续章节中,您将看到它也可能发生在内联大小中。如果确实发生了这种情况,您可以使用 overflow 简写属性来控制情况。

visible的默认值意味着元素的内容可能在元素框外可见。通常,这会导致内容超出其自身元素框,但不会改变该框的形状。下面的标记将导致图 6-18 的结果:

div#sidebar {block-size: 7em; background: #BBB; overflow: visible;}

如果将overflow设置为hidden,元素的内容将被裁剪在元素框的边缘。使用hidden值时,没有办法访问被裁剪掉的内容部分。

如果将overflow设置为clip,元素的内容也将被裁剪——即在元素框的边缘隐藏——没有办法访问被裁剪掉的部分。

如果将overflow设置为scroll,溢出的内容将被裁剪,但用户可以通过滚动方法访问内容,包括滚动条(或一组滚动条)。图 6-18 描绘了其中一种可能性。

如果使用scroll,则应始终呈现平移机制(例如滚动条)。引用规范,“这样可以避免在动态环境中出现滚动条的问题。”因此,即使元素有足够的空间来显示其所有内容,滚动条仍可能出现并占据空间(尽管不一定)。

此外,在打印页面或以其他方式在打印介质中显示文档时,内容可能显示为overflow值被声明为visible

图 6-18 说明了这些overflow值,其中两者结合在一个示例中。

css5 0618

图 6-18. 处理溢出内容的方法

最后,overflow: auto允许用户代理确定要使用先前描述的哪种行为,尽管鼓励用户代理在必要时提供滚动机制。这是一种潜在有用的溢出使用方式,因为用户代理可以将其解释为“仅在需要时提供滚动条”。(它们可能不这样做,但通常是这样的。)

单轴溢出

overflow的简写由两个属性组成。您可以分别设置overflow沿x(水平)和y(垂直)方向的溢出行为,通过设置它们两者在overflow中,或使用overflow-xoverflow-y属性。

通过单独设置每个轴线的溢出行为,你实质上是在决定滚动条将出现在哪里,哪里不出现。考虑下面的情况,在图 6-19 中呈现:

div.one   {overflow-x: scroll; overflow-y: hidden;}
div.two   {overflow-x: hidden; overflow-y: scroll;}
div.three {overflow-x: scroll; overflow-y: scroll;}

css5 0619

图 6-19. 单独为 x 轴和 y 轴设置溢出

在第一种情况下,为 x 轴(水平方向)设置了一个空的滚动条,但对于 y 轴(垂直方向)却没有,尽管内容沿 y 轴溢出。这是两全其美的最坏情况:因为不需要而空置的滚动条,以及在需要时却没有滚动条。

第二种情况是更实用的反例:x 轴上没有滚动条,但是 y 轴上有一个可用的滚动条,因此可以通过滚动访问溢出的内容。

在第三种情况下,两个轴都设置为scroll,因此可以通过滚动访问溢出内容,但是我们也有一个不必要的(空)x 轴滚动条。这相当于简单地声明overflow: scroll

这使我们了解到overflow的真正本质:它是一个将overflow-xoverflow-y合并到一个属性下的简写。以下内容与先前示例完全等效,并将显示与图 6-19 相同的结果:

div.one   {overflow: scroll hidden;}
div.two   {overflow: hidden scroll;}
div.three {overflow: scroll;} /* 'scroll scroll' would also work */

正如您所见,您可以为overflow指定两个关键字,这些关键字始终按顺序为x,然后y。如果只给出一个值,则用于 x 和 y 轴。这就是为什么scrollscroll scroll作为overflow的值是相同的。同样,hidden等同于声明hidden hidden

负边距和折叠

信不信由你,负边距是可能的。其基本效果是将边距边缘向元素框的中心移动。考虑以下内容:

p.neg {margin-block-start: -50px; margin-block-end: 0;
    border: 3px solid gray;}
<div style="width: 420px; background-color: silver; padding: 10px;
 margin-block-start: 50px; border: 1px solid;">
    <p class="neg">
        A paragraph.
    </p>
    A div.
</div>

如我们在图 6-20 中看到的,段落被其负块起始边距向上拉动。请注意,标记中段落后面的<div>的内容也被沿块轴向上拉了 50 像素。

css5 0620

图 6-20. 负块起始边距的效果

现在将以下标记与图 6-21 中显示的情况进行比较:

p.neg {margin-block-end: -50px; margin-block-end: 0;
    border: 3px solid gray;}
<div style="width: 420px; margin-block-start: 50px;">
    <p class="neg">
        A paragraph.
    </p>
</div>
<p>
    The next paragraph.
</p>

css5 0621

图 6-21. 负块结束边距的效果

发生了什么?跟随<div>之后的元素是根据<div>的块结束边距边缘的位置放置的,该位置比没有负边距时高出 50 像素。正如图 6-21 所示,<div>的块结束位置实际上高于其子段落的视觉块结束位置。在<div>之后的下一个元素与<div>的块结束位置之间的距离是适当的。

折叠块轴边距

块轴格式化的一个重要方面是相邻边距的折叠,这是一种比较块方向上相邻边距的方法,然后只使用这些边距中的最大值来设置相邻块元素之间的距离。请注意,折叠行为仅适用于边距。内边距和边框永远不会折叠。

无序列表,列表项沿块轴依次排列,是研究边距折叠的理想环境。假设以下声明适用于包含三个项目的列表:

li {margin-block-start: 10px; margin-block-end: 15px;}

每个列表项具有 10 像素的块起始边距和 15 像素的块末端边距。然而,当列表被渲染时,相邻列表项之间的可见距离是 15 像素,而不是 25 像素。这是因为在块轴上,相邻边距被折叠了。换句话说,较小的边距被舍弃,以较大的边距为准。图 6-22 显示了折叠与未折叠边距的区别。

css5 0622

图 6-22. 折叠与未折叠边距

用户代理将会像图 6-22 中的第一个列表所示折叠块相邻的边距,因此每个列表项之间会出现 15 像素的间隔。第二个列表显示如果浏览器不折叠边距会发生什么,导致列表项之间的间隔为 25 像素。

另一个可以使用的词,如果你不喜欢“折叠”,是“重叠”。虽然边距实际上并没有重叠,但是你可以通过以下类比来想象正在发生的事情。

想象每个元素,比如段落,都是一小张纸,上面写着元素的内容。每张纸周围都有一定数量的透明塑料,代表边距。第一张纸(比如一个 <h1> 纸)被放在画布上。第二张纸(一个段落)被放在它下面沿着块轴,并沿着该轴向上滑动,直到一张纸的边缘的塑料触碰另一张纸的边缘。如果第一张纸的块末端边缘有半英寸的塑料,而第二张纸的块起始有三分之一英寸的塑料,那么当它们滑动在一起时,第一张纸的块末端塑料将接触到第二张纸的块起始边缘。它们现在在画布上被放置完成,并且连接到纸上的塑料是重叠的。

同样会发生折叠,当多个边距相遇时,比如在列表末尾。接下来以早前的例子为例,假设以下规则适用:

ul {margin-block-end: 15px;}
li {margin-block-start: 10px; margin-block-end: 20px;}
h1 {margin-block-start: 28px;}

列表中的最后一项具有 20 像素的块末端边距,<ul> 的块末端边距为 15 像素,而后续 <h1> 的块起始边距为 28 像素。因此,一旦边距被折叠,列表中最后一个 <li> 结束和 <h1> 开始之间的距离为 28 像素,如 图 6-23 所示。

css5 0623

图 6-23. 详细的折叠过程

如果在包含块上添加边框或填充,这将导致其子元素的边距完全包含在其中。我们可以通过在前面示例的 <ul> 元素上添加边框来看到这种行为的运作:

ul {margin-block-end: 15px; border: 1px solid;}
li {margin-block-start: 10px; margin-block-end: 20px;}
h1 {margin-block-start: 28px;}

随着这个改变,<li> 元素的块末端边距现在被放置在其父元素(<ul>)内。因此,唯一发生的边距折叠是在 <ul><h1> 之间,如 图 6-24 所示。

css5 0624

图 6-24. 与添加边框混合(或不混合)的边距合并

负边距合并略有不同。当负边距参与边距合并时,浏览器会取负边距的绝对值,并从任何相邻的正边距中减去它。换句话说,负长度会被添加到正长度中,结果值就是元素之间的距离,即使该距离是负长度。图 6-25 提供了一些具体的例子。

css5 0625

图 6-25. 负块轴边距的示例

现在让我们考虑一个示例,其中列表项、无序列表和段落的边距都被合并。在这种情况下,无序列表和段落被分配负边距:

li {margin-block-end: 20px;}
ul {margin-block-end: -15px;}
h1 {margin-block-start: -18px;}

最大幅度的负边距(-18px)与最大正边距(20px)相加,得到 20px18px = 2px。因此,在列表项内容的块末端和<h1>内容的块开端之间,我们只有 2 像素的间距,正如我们在图 6-26 中看到的。

css5 0626

图 6-26. 边距合并和负边距的详细信息

因为负边距导致元素重叠,很难确定哪些元素位于其他元素之上。您可能还注意到,本节中的示例很少使用背景颜色。如果使用了背景颜色,后续元素的背景颜色可能会覆盖前面元素的内容。这是预期的行为,因为浏览器通常按顺序从开始到结束渲染元素,所以通常可以预期后来文档中的正常流元素会覆盖较早的元素。

内联轴格式化

沿内联轴布局元素可能比您想象的更复杂。这种复杂性的一部分与box-sizing的默认行为有关。在content-box的默认值下,给定的inline-size值影响内容区域的内联宽度,而不是整个可见元素框。考虑以下示例,其中内联轴从左到右:

<p style="inline-size: 200px;">wideness?</p>

这使得段落的内容区域宽度为 200 像素。如果我们给元素添加背景色,这将非常明显。然而,您指定的任何填充、边框或边距都会添加到宽度值上。假设我们这样做:

<p style="inline-size: 200px; padding: 10px; margin: 20px;">wideness?</p>

可见的元素框现在在内联尺寸上为 220 像素,因为我们在内容的每一侧添加了 10 像素的填充。现在边距将在整个元素内联尺寸的两侧再扩展 20 像素。这在图 6-27 中有所说明。

css5 0627

图 6-27. 添加填充和边距

如果我们将样式更改为box-sizing: border-box,结果将会有所不同。在这种情况下,可见框沿着内联轴的宽度为 200 像素,内容内联尺寸为 180 像素,内联两侧共有 40 像素的边距,从而得到总体内联尺寸为 240 像素,如图 6-28 所示。

css5 0628

图 6-28. 减法填充

无论哪种情况,CSS 规范都有一条规则,即块级元素在正常流中的内联组件总是等于包含块的内联尺寸(这也是为什么,稍后您将看到,margin: auto可以在内联方向上居中内容)。让我们考虑一个<div>内的两个段落,其边距已设置为1em,并且其box-sizing值为默认的content-box。在这个例子中,每个段落的内容大小(即inline-size的值)、内联起始和结束填充、边框和边距的总和,将始终加起来等于<div>内容区域的内联尺寸。

假设<div>的内联尺寸是30em。这使得每个段落的内容大小、填充、边框和边距的总和为 30 em。在图 6-29 中,段落周围的“空白”实际上是它们的边距。如果<div>有任何填充,那么将会有更多的空白空间,但这里并非如此。

css5 0629

图 6-29. 元素框的宽度与其包含块的内联宽度一样宽

内联轴属性

内联轴格式化的七个属性——margin-inline-startborder-inline-startpadding-inline-startinline-sizepadding-inline-endpadding-inline-endpadding-inline-end——在图 6-30 中有详细说明。

这七个属性的值必须加起来等于元素包含块的内联尺寸,通常是块元素父级的inline-size的值(因为块级元素几乎总是有块级元素作为父级)。

这七个属性中,只有三个可以设置为auto:元素内容的内联尺寸,以及内联起始和结束边距。其余属性必须设置为具体值或默认为 0。图 6-30 展示了框的哪些部分可以接受auto值,哪些不行。(不过,CSS 很宽容:如果设置了任何不能接受auto的部分为auto,它将默认为0。)

css5 0630

图 6-30. 内联轴格式化的七个属性及可以设置为auto的部分

inline-size属性必须设置为auto或非负值。在内联轴格式化中使用auto时,可能会产生不同的效果。

使用auto

在某些情况下,显式地将一个或多个内联边距和尺寸设置为 auto 是非常有意义的。默认情况下,两个内联边距设置为 0,内联尺寸设置为 auto。让我们探讨一下移动 auto 可以产生不同效果的原因。

只有一个 auto

如果您将 inline-sizemargin-inline-startmargin-inline-end 中的一个设置为 auto,并为其他两个属性指定特定值,则设置为 auto 的属性将设置为使元素框的整体内联尺寸等于父元素的内容内联尺寸所需的长度。

假设七个内联轴属性的总和必须等于 500 像素,没有设置填充或边框,内联尺寸和内联结束边距设置为 100px,内联起始边距设置为 auto。因此,内联起始边距将宽度为 300 像素:

div {inline-size: 500px;}
p {margin-inline-start: auto; margin-inline-end: 100px;
    inline-size: 100px;} /* inline-start margin evaluates to 300px */

在某种意义上,auto 可以用来填补其他所有内容和所需总和之间的差异。然而,如果这三个属性(内联边距和内联尺寸)都设置为 100px,并且没有一个设置为 auto,那会怎么样呢?

如果这三个属性都设置为除 auto 之外的其他值,或者在 CSS 术语中,当这些格式化属性已经过度约束时,内联结束边距将始终被强制为 auto。这意味着如果两个内联边距和内联尺寸都设置为 100px,用户代理将重置内联结束边距为 auto。然后,内联结束边距的宽度将根据一个 auto 值的规则设置,“填充”使元素的整体内联尺寸等于其包含块的内容内联尺寸。图 6-31 显示了类似英语这样的从左到右语言中以下标记的结果:

div {inline-size: 500px;}
p {margin-inline-start: 100px; margin-inline-end: 100px;
    inline-size: 100px;} /* inline-end margin forced to be 300px */

css5 0631

图 6-31. 覆盖内联结束边距的值

如果两侧边距都明确设置,并且 inline-size 设置为 auto,那么 inline-size 将是达到所需总和所需的任何值(即父元素内容的内联尺寸)。以下标记的结果显示在 图 6-32 中:

p {margin-inline-start: 100px; margin-inline-end: 100px;
     inline-size: auto;}

这种类型的格式化是最常见的,因为它等同于设置边距并且不为 inline-size 声明任何内容。以下标记的结果与 图 6-32 中显示的完全相同:

p {margin-inline-start: 100px; margin-inline-end: 100px;} /* same as before */

css5 0632

图 6-32. 自动内联尺寸调整

如果 box-sizing 设置为 padding-box,您可能会想知道会发生什么。在这种情况下,这里描述的所有原理都适用,这就是为什么本节仅讨论 inline-size 和内联侧边距而不引入任何填充或边框的原因。

换句话说,本节和以下各节中对inline-size: auto的处理方式与box-sizing的值无关。在box-sizing定义的框内部放置内容的细节可能有所不同,但auto值的处理方式不变,因为box-sizing决定了inline-size所指的内容,而不是它与边距的关系。

多个自动

现在让我们看看当三个属性中的两个(inline-sizemargin-inline-startmargin-inline-end)都设置为auto时会发生什么。如果两个边距都设置为auto,但inline-size设置为特定长度,则两个边距将设置为相等的长度,从而使元素在其父元素沿内联轴居中。以下代码创建了这种布局,示例显示在图 6-33 中:

div {inline-size: 500px;}
p {inline-size: 300px; margin-inline-start: auto; margin-inline-end: auto;}
  /* each margin is 100 pixels, because (500-300)/2 = 100 */

css5 0633

图 6-33. 设置显式内联尺寸

另一种沿内联轴调整元素大小的方法是将一个内联边距和inline-size设置为auto。在这种情况下,设置为auto的边距将被减少到 0:

div {inline-size: 500px;}
p {margin-inline-start: auto; margin-inline-end: 100px; inline-size: auto;}
  /* inline-start margin evaluates to 0; inline-size becomes 400px  */

然后,inline-size属性被设置为使元素填充其包含块所需的值;在上述示例中,它将是 400 像素,如图 6-34 所示。

css5 0634

图 6-34. 当inline-size和内联起始边距都为auto时的情况

太多的自动

最后,当这三个属性都设置为auto时会发生什么?答案是:两个边距都设置为 0,而inline-size则尽可能宽。这个结果与默认情况相同,即当边距或内联尺寸未明确声明时。在这种情况下,边距默认为 0,而inline-size默认为auto

请注意,由于内联边距不会折叠(与前面讨论的块边距不同),父元素的填充、边框和边距可以影响其子元素的内联布局。这种影响是间接的,即一个元素的边距(等等)可以引起子元素的偏移。以下标记的结果显示在图 6-35 中:

div {padding: 50px; background: silver;}
p {margin: 30px; padding: 0; background: white;}

css5 0635

图 6-35. 偏移在父元素的边距和填充中是隐含的

负边距

正如您在块轴边距中看到的那样,也可以为内联轴边距设置负值。设置负内联边距可能会导致一些有趣的效果。

请记住,七个内联轴属性的总和始终等于父元素内容区域的内联尺寸。只要所有内联属性都是 0 或更大,元素的内联尺寸就永远不会大于其父元素的内容区域内联尺寸。但是,请考虑以下标记,示例显示在图 6-36 中:

div {inline-size: 500px; border: 3px solid black;}
p.wide {margin-inline-start: 10px; margin-inline-end: -50px;
    inline-size: auto;}

css5 0636

图 6-36. 通过负边距使子元素更宽

是的,确实,子元素在内联轴上比其父元素更宽!这在数学上是正确的。让我们解决内联尺寸的问题:

  • 10 px + 0 + 0 + 540 px + 0 + 0 – 50 px = 500 px

540pxinline-size: auto的计算结果,这是平衡方程中其余值所需的数字。即使这导致子元素超出其父元素,所有这些工作都是因为七个属性的值总和达到了所需的总和。

现在,让我们添加一些边框:

div {inline-size: 500px; border: 3px solid black;}
p.wide {margin-inline-start: 10px; margin-inline-end: -50px;
    inline-size: auto; border: 3px solid gray;}

结果的变化将导致inline-size的评估宽度减少:

  • 10 px + 3 px + 0 + 534 px + 0 + 3 px – 50 px = 500 px

或者,我们可以重新排列方程以解决内容大小而不是父元素宽度的问题:

  • 500 px – 10 px – 3 px – 3 px + 50 px = 534 px

如果我们引入填充,inline-size的值将进一步下降(假设box-sizing: content-box)。

相反,可以将auto的内联结束边距计算为负值。如果其他属性的值强制内联结束边距为负,以满足元素不超过其包含块的要求,那么就会发生这种情况。考虑以下情况:

div {inline-size: 500px; border: 3px solid black;}
p.wide {margin-inline-start: 10px; margin-inline-end: auto;
    inline-size: 600px; border: 3px solid gray;}

方程如下所示:

  • 500 px – 10 px – 600 px – 3 px – 3 px = –116 px

在这种情况下,内联结束边距计算为-116px。无论在 CSS 中给定了多少显式值,由于规则规定当元素尺寸过度约束时,内联结束边距将被重置为使数字正确计算的值。

让我们考虑另一个例子,如图 6-37 所示,其中内联起始边距设置为负:

div {inline-size: 500px; border: 3px solid black;}
p.wide {margin-inline-start: -50px; margin-inline-end: 10px;
    inline-size: auto; border: 3px solid gray;}

css5 0637

图 6-37。设置负内联起始边距

具有负内联起始边距的段落不仅超出了<div>的边界,还超出了浏览器窗口的边缘!

记住:填充、边框和内容宽度(和高度)永远不能为负。只有边距可以小于 0。

百分比

关于内联尺寸、填充和边距的百分比值,与前几节讨论的基本规则相同。值是用长度还是百分比声明的并不重要。

百分比可以非常有用。假设我们希望元素的内容占其包含块的内联尺寸的三分之二,填充边为 5%各自,内联起始边距为 5%,而内联结束边距则充当间隙。可以这样写:

<p style="inline-size: 67%;
 padding-inline-end: 5%; padding-inline-start: 5%;
 margin-inline-end: auto; margin-inline-start: 5%;">
     playing percentages</p>

内联结束边距将评估为包含块宽度的 18%(100% – 67% – 5% – 5% – 5%)。

然而,混合百分比和长度单位可能会很棘手。考虑以下例子:

<p style="inline-size: 67%; padding-inline-end: 2em; padding-inline-start: 2em;
 margin-inline-end: auto; margin-inline-start: 5em;">mixed lengths</p>

在这种情况下,元素的框可以定义如下:

  • 5 em + 0 + 2 em + 67% + 2 em + 0 + auto = 包含块宽度

为了使内联末端边距的内联尺寸评估为 0,元素的包含块必须沿内联轴宽 27.272727 em(元素内容区域为 18.272727 em)。如果超过这个宽度,内联末端边距将评估为正值。如果更窄,则内联末端边距将为负值。

情况变得更加复杂,如果我们开始混合长度值单位类型,像这样:

<p style="inline-size: 67%;
 padding-inline-end: 15px; padding-inline-start: 10px;
 margin-inline-end: auto; margin-inline-start: 5em;">more mixed lengths</p>

而且,为了使事情更加复杂,边框不能接受百分比值,只能接受长度值。最后的结论是,如果你不愿意避免使用边框或使用灵活盒布局等方法,那么仅仅基于百分比来创建完全灵活的元素是不可能的。也就是说,如果确实需要混合百分比和长度单位,使用calc()minmax()值函数可能会改变生活,或至少是布局的改变。

替换元素

到目前为止,我们一直在处理文本正常流中非替换块框的内联轴格式化。替换元素管理起来更简单一些。所有适用于非替换块的规则都成立,只有一个例外:如果inline-sizeauto,则元素的inline-size是内容的固有宽度。(“固有”意味着原始大小——当未应用任何外部因素时元素的默认大小。)在以下示例中的图像将为 20 像素宽,因为这是原始图像的宽度:

<img src="smile.svg" style="display: block; inline-size: auto; margin: 0;"
   alt="smile">

如果实际图像宽度为 100 像素,则元素(及其图像)将布局为宽 100 像素。

我们可以通过为inline-size指定特定值来覆盖此规则。假设我们修改前面的示例以显示相同的图像三次,每次都有不同的宽度值:

<img src="smile.svg" style="display: block; inline-size: 25px; margin: 0;"
   alt="small smile" role="img">
<img src="smile.svg" style="display: block; inline-size: 50px; margin: 0;"
   alt="medium smile" role="img">
<img src="smile.svg" style="display: block; inline-size: 100px; margin: 0;"
   alt="large smile" role="img">

图 6-38 说明了结果。

css5 0638

图 6-38。更改替换元素的内联尺寸

请注意,元素的块大小也会增加。当替换元素的inline-size从其固有宽度更改时,block-size的值会按比例缩放以保持对象的初始宽高比,除非block-size已经设置为自己的显式值。反之亦然:如果设置了block-size,但将inline-size保留为auto,那么内联尺寸将按比例缩放以匹配块大小的变化。

列表项

列表项有一些特殊的规则。它们通常由标记符号(如圆点标记或数字)作为前缀。

列表项元素附加的标记可以是列表项内容之外的,也可以作为内容开头的内联标记,具体取决于list-style-position属性的值,如图 6-39 所示。

css5 0639

图 6-39。列表内外的标记

如果标记留在内容外部,则放置在内容的内联起始内容边缘的指定距离处。无论如何修改列表的样式,标记始终保持与内容边缘的相同距离。

记住,列表项框为其后代框定义了包含块,就像常规块框一样。

注意

更详细地讨论了列表标记,包括如何使用::marker 伪元素创建和样式化它们,请参阅第十六章。

具有宽高比的盒模型

有时您可能希望通过其 宽高比 调整元素的大小,这意味着其块和内联尺寸存在特定的比例。例如,旧电视机的宽高比为 4:3;高清视频分辨率的宽高比为 16:9。您可能希望在仍允许其大小灵活的情况下强制元素为正方形。在这些情况下,aspect-ratio 属性可以帮助。

假设我们知道将会有许多元素,但不知道每个元素的宽度或高度,但我们希望它们都是正方形。首先选择一个你想要尺寸的轴。我们将在这里使用height。确保另一个轴是自动调整大小的,并设置一个宽高比:

.gallery div {width: auto; aspect-ratio: 1/1;}

图 6-40 显示相同的一组 HTML,应用了前述的 CSS 和未应用 CSS 的情况。

css5 0640

图 6-40. 带有和不带有定义的宽高比的画廊

根据box-sizing(见“改变盒模型”),距离保持比率,在以下 CSS 给定的情况下,结果将是一个元素,其外边框距离处于精确的 2:1 比率:

.cards div {height: auto; box-sizing: border-box; aspect-ratio: 2/1;}

默认值auto表示具有固有宽高比的框——例如由图像生成的框——将使用该宽高比。对于没有固有宽高比的元素,如大多数 HTML 元素如<div><p>等,框的轴尺寸将由内容决定。

内联格式化

内联格式化 不像格式化块级元素那样简单,后者只生成块级框,并且通常不允许其他内容与之共存。相比之下,看一看 块级元素的内部,例如段落。您可能会问,每行的大小和换行是如何确定的?是什么控制行的排列?我如何影响它?

行布局

要理解行是如何生成的,请首先考虑一个包含一行非常长文本的元素,如图 6-41 所示。请注意,我们通过将整行文本包装在一个<span>元素中并为其分配边框样式来在行周围放置了边框:

span {border: 1px dashed black;}

css5 0641

图 6-41. 单行内联元素

图 6-41 显示了一个内联元素包含在块级元素中的最简单情况。

要从这种简化状态转变为更熟悉的状态,我们只需确定元素沿着内联轴的宽度,然后将行分解,以使结果的片段适合元素的内容内联尺寸。因此,我们得到了 第 6-42 图 所示的状态。

css5 0642

第 6-42 图。一个多行内联元素

事实上并没有什么改变。我们只是将单行文本分成片段,然后沿着块流方向依次堆叠这些片段。

在 第 6-42 图 中,每行文本的边框恰好与每行的顶部和底部重合。这仅在内联文本没有设置填充时才成立。请注意,边框会略微重叠;例如,第一行的底部边框正好在第二行的顶部边框下方。这是因为边框是在每行的外部的下一个像素上绘制的。由于行相互接触,它们的边框如 第 6-42 图 所示重叠。

为了简化起见,我们在讨论线框边缘时使用诸如 topbottom 等术语。在这个语境中,线框的顶部是最靠近块起始端的一侧,线框的底部是最靠近块结束端的一侧。类似地,tallshort 指的是沿着块轴的线框大小。

如果我们改变跨度样式以具有背景颜色,则行的实际放置变得更加清晰。考虑 第 6-43 图,显示了每种写作模式中的两个段落,以及不同 text-align 值的效果(参见 第 15 章),每个段落的行的背景都填充了。

css5 0643

第 6-43 图。展示不同对齐方式和写作模式中的行

如 第 6-43 图 所示,并非每一行都延伸到其父段落的内容区域边缘,这已用虚线灰色边框表示。对于左对齐的段落,所有行都紧贴段落的左内容边缘,并且每行的结尾位置取决于行的断开处。右对齐段落的情况正好相反。对于居中对齐的段落,每行的中心与段落的中心对齐。

在最后一种情况下,当 text-align 的值为 justify 时,每行(除了最后一行)被强制扩展到段落的内容区域宽度,以使行的边缘接触段落的内容边缘。行的自然长度与段落内容区域宽度之间的差异通过改变每行中字母和单词之间的间距来弥补。因此,在文本进行两端对齐时,word-spacing 的值可以被覆盖。(如果是长度值,则不能覆盖 letter-spacing 的值。)

这基本上涵盖了最简单情况下行的生成方式。然而,正如你即将看到的,行内格式模型远非简单。

基本术语和概念

在我们进一步讨论之前,让我们回顾一些行内布局术语,这些术语在后续章节中导航中至关重要:

匿名文本

任何未包含在内联元素中的字符序列。因此,在标记中<p> I'm <em>so</em> happy!</p>,序列“I’m”和“happy!”是匿名文本。请注意,空格是文本的一部分,因为空格像其他字符一样。

em 盒子

这是在给定字体中定义的,也称为字符框。实际字形可以比其 em 盒子更高或更矮。在 CSS 中,font-size的值决定了每个 em 盒子的高度。

内容区

在非替换元素中,内容区可以是两者之一,而 CSS 规范允许用户代理选择其中之一。内容区可以是每个字符的 em 盒子串在一起描述的盒子;或者可以是元素中字符字形描述的盒子。在本书中,我们为了简单起见使用 em 盒子的定义,这也是大多数浏览器使用的方式。在替换元素中,内容区是元素的固有高度加上任何边距、边框或填充。

Leading

行距(发音为“led-ing”)是font-sizeline-height值之间的差异。这种差异被分成两半,一半应用于内容区的顶部,另一半应用于底部。这些添加到内容区的部分被称为half-leading。行距仅适用于非替换元素。

行内框

这是通过将行距添加到内容区而描述的框。对于非替换元素,元素的行内框的高度将完全等于line-height属性的值。对于替换元素,元素的行内框的高度将完全等于内容区,因为行距不适用于替换元素。

行框

这是限定行内框的最短框,其边界包围着行中找到的最高和最低点的行内框。换句话说,行框的顶边缘位于最高行内框顶部的顶部,底边缘位于最低行内框底部的底部。请记住,“顶部”和“底部”是相对于块流方向来考虑的。

CSS 还包含一组超出上述术语和定义范围的行为和有用概念:

  • 行内框的内容区类似于块框的内容框。

  • 行内元素的背景应用于内容区加任何填充。

  • 任何行内元素上的边框围绕内容区加任何填充。

  • 非替换内联元素的填充、边框和边距对内联元素或它们生成的框没有垂直影响;它们不会影响元素的内联框高度(因此也不会影响包含该元素的行框)。

  • 替换元素的边距和边框确实会影响该元素的内联框的高度,从而影响包含该元素的行的行框的高度。

还有一件事要注意:内联框根据它们的 vertical-align 属性值在行内垂直对齐(见第十五章)。

在继续之前,让我们逐步查看构建行框的过程,您可以通过该过程查看如何将行的各个部分组合在一起以确定其高度。按照以下步骤确定每个元素在行中的内联框高度:

  1. 找到每个内联非替换元素和不属于后代内联元素的文本的font-sizeline-height的值,并结合它们。这是通过减去font-sizeline-height得到的,得到盒子的 leading。将 leading 分成两半,分别应用于每个 em 盒子的顶部和底部。

  2. 找到height的值,以及每个替换元素的 block-start 和 block-end 边缘的边距、填充和边框的值,并将它们相加。

  3. 确定每个内容区域,整体行基线上方和下方有多少。这并不是一件容易的事情:您必须知道每个元素和匿名文本的基线位置以及行本身的基线,然后将它们对齐。此外,替换元素的 block-end 边缘位于整体行的基线上。

  4. 确定具有vertical-align值的任何元素的垂直偏移量。这将告诉您该元素的内联框在块轴上将被移动多远,这将改变元素上方或下方的部分量。

  5. 现在您知道所有内联框已经停留在哪里,请计算最终行框的高度。要做到这一点,只需将基线与最高内联框顶部之间的距离与基线与最低内联框底部之间的距离相加。

让我们详细考虑整个过程,这是明智地为内联内容设置样式的关键。

行高度

首先,要知道所有元素都有一个 line-height,无论是否显式声明。这个值极大地影响内联元素的显示方式,因此让我们给予它应有的关注。

行高(或行盒的高度)由其组成元素和文本等内容的高度决定。重要的是要理解,line-height影响的是行内元素和其他行内内容,而不是直接影响块级元素。我们可以为块级元素设置line-height值,但该值仅在应用于该块级元素内的行内内容时才会有视觉影响。例如,考虑以下空段落:

<p style="line-height: 0.25em;"></p>

没有内容,段落将无法显示任何内容,因此我们看不到任何东西。事实上,这个段落具有任何值的line-height——无论是0.25em还是25in——都没有任何差别,没有内容来创建行盒。

我们可以为块级元素设置line-height值,并使其适用于块内的所有内容,无论它们是包含在行内元素中还是匿名文本中。从某种意义上说,块级元素内的每行文本都是其自己的行内元素,无论是否被标签包围。如果您愿意,可以想象一个类似以下的虚构标签序列:

<p>
<line>This is a paragraph with a number of</line>
<line>lines of text that make up the</line>
<line>contents.</line>
</p>

即使实际上line标签并不存在,段落的行为却好像它们存在一样,并且每行文本都从段落中继承样式。您只需要为块级元素创建line-height规则,以便无需显式地为其所有行内元素(无论是虚构的还是其他的)声明line-height

虚构的line元素阐明了在块级元素上设置line-height后产生的行为。根据 CSS 规范,声明块级元素上的line-height设置了该块级元素内容的最小行盒高度。声明p.spacious {line-height: 24pt;}意味着每个行盒的最小高度为 24 点。技术上,只有行内元素才能继承这个行高。大多数文本并不由行内元素包含。如果假设每行都由虚构的line元素包含,这个模型会非常好地工作。

行内非替换元素

在我们的格式知识基础上,让我们继续构建只包含非替换元素(或匿名文本)的行。然后,您将能够很好地理解行内布局中非替换元素和替换元素之间的区别。

注意

在本节中,我们使用topbottom来标记半行导向放置的位置以及如何将行盒放在一起。始终记住,这些术语是相对于块流动方向的:行内框的顶边是最靠近块开始边的边,行内框的底边是最靠近其块结束边的边。类似地,height意味着沿着行内框的块轴的距离,width是沿着其行内轴的距离。

建立方框

首先,对于内联非替换元素或匿名文本片段,font-size的值确定了内容区域的高度。如果一个内联元素的font-size15px,那么内容区域的高度为 15 像素,因为元素中所有的 em 框都是 15 像素高,如图 6-44 所示。

css5 0644

图 6-44. Em 框确定内容区域高度

考虑的下一件事是元素的line-height值,以及它与font-size值之间的差异。如果一个内联非替换元素的font-size15pxline-height21px,那么差异为 6 像素。用户代理程序将这 6 像素分成两半,将其中一半(3 像素)应用于内容区域的顶部,另一半(3 像素)应用于底部,从而形成内联框。图 6-45 说明了这个过程。

css5 0645

图 6-45. 内容区域加上行距等于内联框

现在,让我们打破一些东西,以便更好地理解行高的工作原理。假设以下内容是真实的:

<p style="font-size: 12px; line-height: 12px;">
This is text, <em>some of which is emphasized</em>, plus other text<br>
that is <strong style="font-size: 24px;">strongly emphasized</strong>
and that is<br>
larger than the surrounding text.
</p>

在这个例子中,大部分文本的font-size12px,而一个内联非替换元素中的文本大小为24px。然而,所有文本的line-height都为12px,因为line-height是继承属性。因此,<strong>元素的line-height也为12px

因此,对于每个font-sizeline-height都为12px的文本片段,内容高度不会改变(因为12px12px之间的差异为 0),因此内联框高度为 12 像素。然而,对于加粗文本,line-heightfont-size之间的差异为-12px。这被分成两半来确定半行距(-6px),半行距被添加到内容高度的顶部和底部,得到内联框。由于我们在两种情况下都添加了负数,内联框的高度最终为 12 像素。12 像素的内联框在元素的 24 像素内容高度内垂直居中,因此内联框比内容区域小。

到目前为止,听起来我们对每一小段文本都做了相同的处理,所有的内联框都是相同的大小,但这并不完全正确。第二行的内联框虽然大小相同,但因为文本都是基线对齐的(见图 6-46),它们并不对齐。这是我们稍后在本章讨论的一个概念。

由于内联框决定了整体行框的高度,它们相互之间的放置至关重要。行框被定义为从行中最高内联框的顶部到最低内联框的底部的距离,并且每行框的顶部紧靠上一行框的底部。

在 图 6-46 中,一行文本中放置了三个框:两个匿名文本框位于 <strong> 元素两侧,以及 <strong> 元素本身。因为包围段落的 line-height12px,每个框的内联框都是 12 像素高。这些内联框在每个框的内容区域内居中。然后它们的基线对齐,因此所有文本共享一个公共基线。

但是由于内联框相对于这些基线的位置,<strong> 元素的内联框比匿名文本框的内联框稍高。因此,从 <strong> 的内联框顶部到匿名内联框底部的距离超过了 12 像素,而行的可见内容并没有完全包含在行框内。

css5 0646

图 6-46. 行内框在一行中

终于,在所有这些操作之后,文本的中间行放置在其他两行文本之间,如 图 6-47 所示。第一行文本的底部边缘与我们在 图 6-46 中看到的文本行的顶部边缘对齐。同样,第三行文本的顶部边缘与中间文本行的底部边缘对齐。由于中间文本行的行框略高,结果导致文本行看起来不规则,因为三个基线之间的距离不一致。

css5 0647

图 6-47. 段落内的行框
注意

稍后我们将探讨应对这种基线不规则分隔和实现一致基线间距的方法。(剧透:无单位值赢!)

设置垂直对齐

如果我们改变内联框的垂直对齐方式,相同的高度确定原则也同样适用。假设我们给 <strong> 元素一个垂直对齐方式为 4px

<p style="font-size: 12px; line-height: 12px;">
This is text, <em>some of which is emphasized</em>, plus other text<br>
that is <strong style="font-size: 24px; vertical-align: 4px;">strongly
emphasized</strong>  and that is<br>
larger than the surrounding text.
</p>

这个小改变使 <strong> 元素上升了 4 像素,从而将其内容区域和内联框都推高了。因为 <strong> 元素的内联框顶部已经是行中最高的,这种垂直对齐方式的改变也将行框的顶部向上推了 4 像素,如 图 6-48 所示。

css5 0648

图 6-48. 垂直对齐影响行框高度
注意

vertical-align 的正式定义可以在 第十五章 中找到。

让我们考虑另一种情况。这里,我们有另一个内联元素与强调文本在同一行,其对齐方式与基线不同:

<p style="font-size: 12px; line-height: 12px;">
This is text, <em>some of which is emphasized</em>,<br>
plus other text that is <strong style="font-size: 24px; vertical-align: 4px;">
strong</strong> and <span style="vertical-align: top;">tall</span> and is<br>
larger than the surrounding text.
</p>

现在我们得到了与早期示例中相同的结果,即中间的行框比其他行框更高。但请注意,图 6-49 中“高”文本的对齐方式。

css5 0649

图 6-49. 将内联元素对齐到行框

在这种情况下,“高”文本的内联框顶部与行框的顶部对齐。由于“高”文本的 font-sizeline-height 值相等,内容高度和内联框是一样的。但请考虑这一点:

<p style="font-size: 12px; line-height: 12px;">
This is text, <em>some of which is emphasized</em>,<br>
plus other text that is <strong style="font-size: 24px; vertical-align: 4px;">
strong</strong> and <span style="vertical-align: top; line-height: 2px;">
tall</span> and is<br> larger than the surrounding text.
</p>

由于“高”文本的 line-height 小于其 font-size,因此该元素的内联框小于其内容区域。这个微小的差异改变了文本本身的位置,因为其内联框的顶部必须与其行的行框顶部对齐。因此,我们得到了 图 6-50 中显示的结果。

css5 0650

图 6-50. 文本从行框中突出显示(再次)

根据本章使用的术语,vertical-align 的各种关键字值的效果如下:

top

将元素内联框的顶部(块起始边缘)与包含行框的顶部对齐。

bottom

将元素内联框的底部(块结束边缘)与包含行框的底部对齐。

text-top

将元素内联框的顶部(块起始边缘)与父元素内容区域的顶部对齐。

text-bottom

将元素内联框的底部(块结束边缘)与父元素内容区域的底部对齐。

middle

将元素内联框的垂直中点与父元素基线以上 0.5ex 处对齐。

super

将元素的内容区域和内联框沿着块轴向上移动。距离未指定,可能会因用户代理而异。

sub

super 相同,只是元素沿块轴向下移动而不是向上移动。

<percentage>

将元素沿块轴上下移动,距离由以声明百分比计算的 line-height 值定义。

管理行高

在前面的章节中,您看到更改内联元素的 line-height 可导致一行文字与另一行重叠。不过,每种情况下都是对单个元素进行更改。那么,如何以更一般的方式影响元素的 line-height 以防止内容重叠呢?

一种方法是将 em 单位与已更改 font-size 的元素结合使用。例如:

p {line-height: 1em;}
strong {font-size: 250%; line-height: 1em;}
<p>
Not only does this paragraph have "normal" text, but it also<br>
contains a line in which <strong>some big text</strong> is found.<br>
This large text helps illustrate our point.
</p>

通过为 <strong> 元素设置 line-height,我们增加了行框的总高度,提供足够的空间来显示 <strong> 元素,而不会与任何其他文本重叠,并且不会更改段落中所有行的 line-height。我们使用 1em 的值,以便 <strong> 元素的 line-height 将设置为与 <strong>font-size 相同的大小。请记住,line-height 是相对于元素本身的 font-size 设置的,而不是父元素。图 6-51 显示了结果。

css5 0651

图 6-51. 将 line-height 属性分配给内联元素

确保你真正理解了前面的章节,因为当我们尝试添加边框时,文本的可读格式会变得更加棘手。假设我们想在任何超链接周围加上 5 像素的边框:

a:any-link {border: 5px solid blue;}

如果我们没有设置足够大的line-height来容纳边框,它可能会危及其他行。我们可以通过使用line-height来增加超链接的内联框大小,就像我们在前面的<strong>元素示例中所做的那样;在这种情况下,我们只需使超链接的line-height值比其font-size值大 10 像素即可。然而,如果我们实际上不知道字体的像素大小,这将会很困难。

另一个解决方案是增加段落的line-height。这将影响整个元素中的每一行,而不仅仅是包含边框超链接的行:

p {line-height: 1.8em;}
a:link {border: 5px solid blue;}

因为在每行上方和下方都添加了额外的空间,所以超链接周围的边框不会影响任何其他行,如图 6-52 所示。

css5 0652

图 6-52。增加line-height以留出内联边框的空间

这种方法有效是因为所有文本大小相同。如果行中包含其他改变行框高度的元素,我们的边框情况也可能会改变。考虑以下情况:

p {font-size: 14px; line-height: 24px;}
a:link {border: 5px solid blue;}
strong {font-size: 150%; line-height: 1.5em;}

根据这些规则,在段落中<strong>元素的内联框高度将为 31.5 像素(14 × 1.5 × 1.5),并且这也将是行框的高度。为了保持基线间距一致,我们必须使<p>元素的line-height大于或等于32px

理解基线和行高

每个行框的实际高度取决于其组成元素如何垂直排列。这种对齐往往极大地依赖于基线在每个元素(或匿名文本片段)中的位置,因为这个位置决定了内联框如何垂直排列。

一致的基线间距往往更像是一门艺术而非科学。如果你使用单一单位(例如 em)声明所有的字体大小和行高度,那么你有很大机会实现一致的基线间距。然而,如果你混合使用单位,这个目标将变得更加困难,甚至不可能。

截至 2022 年底,有建议提出了一些属性,可以让作者在不考虑内联内容的情况下强制实现一致的基线间距,这将极大地简化在线排版的某些方面。然而,这些建议的属性尚未实施,这使得它们的采纳最多只是一个遥远的希望。

调整行高度

设置line-height的最佳方式是使用原始数字作为值。这种方法最好,因为数字成为缩放因子,而且是继承的,而不是计算的值。假设我们希望文档中所有元素的line-height为它们font-size的 1.5 倍。我们将声明如下:

body {line-height: 1.5;}

这个缩放因子为 1.5 会从一个元素传递到另一个元素,并且在每个级别上,该因子将被用作每个元素font-size的乘数。因此,以下标记将显示为图 6-53 所示:

p {font-size: 15px; line-height: 1.5;}
small {font-size: 66%;}
strong {font-size: 200%;}

<p>This paragraph has a line-height of 1.5 times its font-size. In addition,
any elements within it <small>such as this small element</small> also have
line-heights 1.5 times their font-size...and that includes <strong>this big
element right here</strong>. By using a scaling factor, line-heights scale
to match the font-size of any element.</p>

在这个例子中,<small>元素的行高为 15 像素,<strong>元素的行高为 45 像素。如果我们不希望我们的大<strong>文本生成太多额外的行间距,我们可以给它自己的line-height值,这将覆盖继承的缩放因子。

p {font-size: 15px; line-height: 1.5;}
small {font-size: 66%;}
strong {font-size: 200%; line-height: 1em;}

css5 0653

图 6-53. 使用缩放因子设置line-height

向非替换元素添加框属性

正如您可能记得之前的讨论,虽然内边距、边距和边框都可以应用于内联非替换元素,但这些属性对内联元素的线框高度没有影响。

内联元素的边框边缘由font-size控制,而不是line-height。换句话说,如果一个<span>元素的font-size12pxline-height36px,那么其内容区域高度为12px,边框将围绕该内容区域。

或者,我们可以给内联元素分配内边距,这将使边框远离文本本身:

span {padding: 4px;}

此内边距不会改变内容高度的实际形状,因此它不会影响此元素的内联框高度。类似地,向内联元素添加边框也不会影响生成和布局线框的方式,如图 6-54 所示(无论是否有 4 像素内边距)。

css5 0654

图 6-54. 内边距和边框不会改变行高

至于边距,它们在实际上并不适用于内联非替换元素的块边缘,因为它们不影响线框的高度。元素的内联结束是另一回事。

记住内联元素基本上是作为单行布局然后被分割成片段的概念。因此,如果我们给内联元素应用了边距,这些边距将出现在其起始和结束位置:这些分别是内联起始和内联结束边距。内边距也出现在这些边缘。因此,尽管内边距和边距(以及边框)不会影响行高,它们仍然可以通过将文本推离其端点来影响元素内容的布局。事实上,负的内联起始和结束边距可以将文本拉近至内联元素,甚至造成重叠。

那么,当内联元素具有背景并且有足够的内边距使得行的背景重叠时会发生什么?请看以下情况:

p {font-size: 15px; line-height: 1em;}
p span {background: #FAA;
     padding-block-start: 10px; padding-block-end: 10px;}

<span> 元素内的所有文本将具有 15 像素高的内容区域,我们已经在每个内容区域的顶部和底部应用了 10 像素的填充。额外的像素不会增加行框的高度,这通常没问题,但是有一个背景颜色。因此,我们得到了 Figure 6-55 中显示的结果。

css5 0655

图 6-55. 内联元素的填充和边距

CSS 明确指出行框按文档顺序绘制:“这将导致后续行的边框绘制覆盖前面行的边框和文本。”相同的原则也适用于背景,如 Figure 6-55 所示。

改变断裂行为

在前一节中,您看到当内联非替换元素跨多行断裂时,它会被视为一个长的单行元素,被切割成较小的框,每行断裂一个切片。这只是默认行为,可以通过 box-decoration-break 属性进行更改。

默认值 slice 是您在前一节中看到的内容。另一个值 clone 会导致元素的每个片段被绘制为独立的框。这是什么意思?比较 Figure 6-56 中的两个示例,即使是相同的标记和样式,它们也会被视为切片或克隆处理。

许多差异可能显而易见,但也有一些可能更微妙。其中影响之一是在每个元素片段上应用填充,包括行断裂处的末端。类似地,边框会在每个片段周围单独绘制,而不是分割开来。

css5 0656

图 6-56. 内联片段的切片和克隆

更微妙的是,注意在两者之间 background-image 的定位方式如何变化。在切片版本中,背景图像会随一切切片,这意味着只有一个片段包含原始图像。然而,在克隆版本中,每个背景都作为自己的副本,因此每个都有自己的原始图像。这意味着,例如,即使我们有一个不重复的背景图像,在每个片段中它也会出现一次,而不仅仅是一个片段中。

box-decoration-break 属性通常与内联框一起使用,但在元素中有断裂时,例如分页媒体中的页面断裂,它适用于任何时候。在这种情况下,每个片段都是单独的切片。如果我们设置 box-decoration-break: clone,那么每个框片段在边框、填充、背景等方面都将被视为副本处理。在多列布局中同样适用:如果元素被列断裂,box-decoration-break 的值将影响其呈现方式。

符号与内容区域

即使您试图防止内联非替换元素的背景重叠,这种情况仍可能发生,这取决于所使用的字体。问题在于字体的 em 盒子与其字符字形之间的差异。事实证明,大多数字体的 em 盒子的高度与字符字形并不匹配。

这听起来可能有些抽象,但它确实有实际的影响。内联非替换元素的“绘画区域”由用户代理确定。如果用户代理将 em 盒子视为内容区域的高度,内联非替换元素的背景将等于 em 盒子的高度(即font-size的值)。如果用户代理使用字体的最大上升和下降,背景可能会比 em 盒子更高或更低。因此,您可以给内联非替换元素设定line-height1em,仍然使其背景与其他行的内容重叠。

内联替换元素

内联替换元素(例如图像)被假定具有固有的高度和宽度;例如,图像将有特定数量的像素高和宽。因此,具有固有高度的替换元素可以使行框变得比正常更高。这不会改变任何行中元素的line-height值,包括替换元素本身。相反,行框的高度刚好足以容纳替换元素,再加上任何盒子属性。换句话说,整个替换元素——内容、边距、边框和填充——用于定义元素的内联框。下列样式导致这种情况的一个示例,如图 6-57 所示:

p {font-size: 15px; line-height: 18px;}
img {block-size: 30px; margin: 0; padding: 0; border: none;}

css5 0657

图 6-57. 替换元素可以增加行框的高度,但不会改变line-height的值

尽管有大量的空白空间,line-height的有效值对于段落或图像本身都没有变化。line-height的值对图像的内联框没有影响。因为图 6-57](#bvf_fig51)中的图像没有填充、边距或边框,它的内联框等同于其内容区域,在这种情况下为 30 像素高。

尽管如此,内联替换元素仍然具有line-height的值。为什么呢?在最常见的情况下,它需要这个值才能正确地定位元素,如果它已经被垂直对齐。请回忆一下,例如,vertical-align的百分比值是相对于元素的line-height计算的。因此:

p {font-size: 15px; line-height: 18px;}
img {vertical-align: 50%;}
<p>The image in this sentence <img src="test.gif" alt="test">
will be raised 9 pixels.</p>

line-height的继承值导致图像上升了 9 像素,而不是其他数字。如果没有line-height的值,就不可能执行百分比值的垂直对齐。图像本身的高度对垂直对齐没有影响;只有line-height的值才重要。

然而,对于其他替换元素而言,将 line-height 值传递给该替换元素内部的后代元素可能是重要的。例如,SVG 图像可以使用 CSS 样式化图像内的文本。

向替换元素添加框属性。

经过我们刚刚经历的一切,向行内替换元素应用外边距、边框和填充似乎几乎是简单的事情。

填充和边框像往常一样应用于替换元素;填充在实际内容周围插入空间,而边框则围绕填充。此过程的不寻常之处在于,填充和边框实际上会影响行框的高度,因为它们是行内替换元素的行内框的一部分(与行内非替换元素不同)。考虑 图 6-58,其结果由以下样式产生:

img {block-size: 50px; inline-size: 50px;}
img.one {margin: 0; padding: 0; border: 3px dotted;}
img.two {margin: 10px; padding: 10px; border: 3px solid;}

注意,第一行框被调整高度足以容纳图像,而第二行框则足以容纳图像、其填充和边框。

css5 0658

图 6-58. 向行内替换元素添加填充、边框和外边距会增加其行内框。

外边距也包含在行框内,但它们有自己的规则。设置正外边距并不神秘;它会使替换元素的行内框变得更高。设置负外边距具有类似效果:它会减小替换元素行内框的大小。这在 图 6-59 中有所说明,我们可以看到,负上外边距拉低了图像上方的行:

img.two {margin-block-start: -10px;}

负外边距在块级元素上的操作方式与本章前面展示的相同。在这种情况下,负外边距使替换元素的行内框小于普通情况。负外边距是使行内替换元素溢出到其他行的唯一方法,这也是为什么替换行内元素生成的框通常被认为是内联块的原因。

css5 0659

图 6-59. 负外边距对行内替换元素的影响。

替换元素和基线。

到目前为止,您可能已经注意到,默认情况下,行内替换元素位于基线上。如果您向替换元素添加底部(块端)填充、外边距或边框,则内容区域将沿块轴向上移动。替换元素本身没有基线,因此下一个最好的方法是将其行内框的底部与基线对齐。因此,实际上是外部块端边缘与基线对齐,如 图 6-60 所示。

css5 0660

图 6-60. 行内替换元素位于基线上。

这种基线对齐导致了一个意外(和不受欢迎)的后果:放置在表格单元格中的图像本身应该使表格单元格足够高以容纳包含图像的行框。即使表格单元格中没有实际的文本,甚至没有空格,也会发生调整。因此,过去年代常见的切片图像和间隔 GIF 设计在现代浏览器中可能会出现显著的问题。(我们知道不会创建这样的东西,但这仍然是解释此行为的一个方便的上下文。)考虑最简单的情况:

td {font-size: 12px;}
<td><img src="spacer.gif" height="1" width="10" alt=""></td>

在 CSS 内联格式化模型下,表格单元格的高度将为 12 像素,图像位于单元格的基线上。因此,图像下方可能有 3 像素的空间,上方有 8 像素,尽管确切的距离取决于使用的字体系列和其基线的放置。

这种行为不仅限于表格单元格内的图像;任何时候内联替换元素是块级或表格单元格元素的唯一后代时都会发生。例如,<div> 中的图像也会位于基线上。

这里有另一个内联替换元素放置在基线上的有趣效果:如果我们应用负底部(块末端)边距,元素会被拉向下移动,因为其内联框的底部比内容区域的底部更高。因此,以下规则将会产生图 6-61 所示的结果:

p img {margin-block-end: -10px;}

css5 0661

图 6-61. 使用负块末端边距拉动内联替换元素

这很容易导致替换元素溢出到后续文本行中,正如图 6-61 所示。

内联块元素

如同其值名inline-block的混合外观所需,内联块元素确实是块级和内联元素的混合体。

内联块元素与其他元素和内容的关系就像一个图像一样作为内联框进行格式化:内联块元素在行内被格式化为替换元素。这意味着内联块元素的底部(块末端)边缘默认会放在文本行的基线上,并且不会在自身内部换行。

在内联块元素中,内容的格式就像该元素是块级元素一样。widthheight 属性适用于该元素(因此也适用于任何块级或内联替换元素),如果它们比周围内容更高,则这些属性将增加行的高度。

让我们考虑一些可以帮助理解的示例标记:

<div id="one">
   This text is the content of a block-level element. Within this
   block-level element is another block-level element.  <p>Look, it's a
   block-level paragraph.</p> Here's the rest of the DIV, which is still
   block-level.
</div>
<div id="two">
   This text is the content of a block-level element. Within this
   block-level element is an inline element.  <p>Look, it's an inline
   paragraph.</p>  Here's the rest of the DIV, which is still block-level.
</div>
<div id="three">
   This text is the content of a block-level element. Within this
   block-level element is an inline-block element.  <p>Look, it's an inline-block
   paragraph.</p>  Here's the rest of the DIV, which is still block-level.
</div>

对这个标记应用以下规则:

div {margin: 1em 0; border: 1px solid;}
p {border: 1px dotted;}
div#one p {display: block; inline-size: 6em; text-align: center;}
div#two p {display: inline; inline-size: 6em; text-align: center;}
div#three p {display: inline-block; inline-size: 6em; text-align: center;}

图 6-62 描述了这个样式表的结果。

css5 0662

图 6-62. 内联块元素的行为

注意第二个 <div> 中,内联段落被格式化为普通的内联内容,这意味着 widthtext-align 被忽略了(因为它们不适用于内联元素)。然而,在第三个 <div> 中,内联块段落却遵循这两个属性,因为它被格式化为块级元素。这段落的边距也会使得其文本行变得更高,因为它影响了行高,就像它是一个替换元素一样。

如果内联块元素的 width 未定义或明确声明为 auto,则元素框将会收缩以适应内容。元素框的宽度正好足够容纳内容,不会更宽。内联框的行为相同,尽管它们可以跨越文本的行,而内联块元素则不能。因此,我们有以下规则,适用于前面的标记示例:

div#three p {display: inline-block; block-size: 4em;}

这将创建一个足够高以容纳内容的宽箱子,如 图 6-63 所示。

css5 0663

图 6-63. 内联块元素的自动调整大小

流动显示

displayflowflow-root 值值得解释一下。声明一个元素使用 display: flow 表示它应该使用块和内联布局,与普通的布局一样——即,除非与 inline 结合使用,否则它生成一个内联框。

换句话说,前两个规则将导致一个块级框,而第三个则产生一个内联框:

#first {display: flow;}
#second {display: block flow;}
#third {display: inline flow;}

这种模式的原因是,CSS 正(非常)缓慢地向支持两种显示方式的系统迁移:外部显示类型内部显示类型。像 blockinline 这样的值关键字代表了外部显示类型,它决定了显示框如何与其周围环境交互。而内部显示类型(在这种情况下是 flow),描述了元素内部的行为。

这种方法允许声明如 display: inline block 的属性来指示一个元素在内部生成一个块级格式化上下文,但与其周围的内容关联作为一个内联元素。(这种新的双术语 display 值与完全支持的 inline-block 值具有相同的效果。)

另一方面,使用 display: flow-root 总是生成一个块级框,在其内部生成一个新的块级格式化上下文。这类似于应用于文档的根元素 <html>,以表示:“这是格式化根的位置。”

你可能熟悉的旧 display 值仍然可用。表 6-1 展示了如何使用新值来表示旧值。

表 6-1. 等效的 display

旧值 新值
block block flow
inline inline flow
inline-block inline flow-root
list-item list-item block flow
inline-list-item list-item inline flow
table block table
inline-table inline table
flex block flex
inline-flex inline flex
grid block grid
inline-grid inline grid

内容显示

display的一个迷人新添加值是contents。当应用于一个元素时,display: contents会导致该元素从页面格式中移除,并有效地“提升”其子元素至其级别。例如,请考虑以下基本的 CSS 和 HTML:

ul {border: 1px solid red;}
li {border: 1px solid silver;}
<ul>
<li>The first list item.</li>
<li>List Item II: The Listening.</li>
<li>List item the third.</li>
</ul>

这会产生一个带有红色边框的无序列表,以及带有银色边框的三个列表项。

如果我们将display: contents应用于<ul>元素,则用户代理将渲染列表,就好像从文档源中删除了<ul></ul>行一样。图 6-64 显示了常规结果与contents结果之间的差异。

css5 0664

图 6-64. 一个常规无序列表和一个带有display: contents的列表

列表项仍然是列表项,并且像列表项一样运作,但在视觉上,<ul>已经消失,就好像它从未存在过一样。不仅列表的边框消失了,通常分隔列表与周围内容的顶部和底部边距也消失了。这就是为什么图 6-64 中的第二个列表显示比第一个列表更高的原因。

其他显示值

在本章中,我们并没有涵盖许多其他显示值,并且不会继续讨论。各种与表格相关的值将在第 13 章中出现,我们将在第 16 章中再次讨论列表项。

我们不会真正讨论的值是与 Ruby 相关的值,这些值需要它们自己的书籍,并且截至 2022 年晚期支持不佳。

元素可见性

除了我们在本章中讨论的所有内容之外,您还可以控制整个元素的可见性。

如果将元素设置为visibility: visible,它就是可见的,正如您可能期望的那样。如果将元素设置为visibility: hidden,它就会被“隐藏”(使用规范中的措辞)。在其不可见状态下,该元素仍然会影响文档的布局,就像它是visible一样。换句话说,元素仍然存在——只是你看不到它。

注意这与display: none的区别。在后一种情况下,元素不会显示并且会从文档中完全移除,因此不会对文档布局产生任何影响。图 6-65 展示了一个文档,其中段落内的内联元素已根据以下样式和标记设置为hidden

em.trans {visibility: hidden; border: 3px solid gray; background: silver;
    margin: 2em; padding: 1em;}
<p>
    This is a paragraph that should be visible. Nulla berea consuetudium ohio
    city, mutationem dolore. <em class="trans">Humanitatis molly shannon
    ut lorem.</em> Doug dieken dolor possim south euclid.
</p>

css5 0665

图 6-65. 使元素在不抑制其元素框的情况下变得不可见

关于隐藏元素的所有可见部分——例如内容、背景和边框——都会变得不可见。空间仍然存在,因为该元素仍然是文档布局的一部分。我们只是看不到它。

我们可以将hidden元素的后代元素设置为visible。这会导致元素在通常情况下出现,即使祖先元素不可见。为此,我们明确声明后代元素为visible,因为visibility是继承的:

p.clear {visibility: hidden;}
p.clear em {visibility: visible;}

至于visibility: collapse,这个值在 CSS 表格渲染和弹性盒布局中使用,其效果与display: none非常相似。区别在于,在表格渲染中,设置为visibility: hidden的行或列是隐藏的,并且它本来占据的空间被移除,但任何在隐藏的行或列中的单元格用于确定相交列或行的布局。这允许您快速隐藏或显示行和列,而不会强制浏览器重新计算整个表格的布局。

如果collapse应用于不是弹性项目或表的一部分的元素,则其具有与hidden相同的含义。

动画化可见性

如果你想要将可见性从可见状态动画到visibility的其他值之一,是可以的。问题在于你不会看到从一个值慢慢过渡到另一个值。相反,浏览器会计算动画中从01(或反之)的变化点,然后立即在那一点改变visibility的值。因此,如果一个元素被设置为visibility: hidden,然后动画到visibility: visible,该元素在达到结束点之前将完全不可见,到达结束点时立即变为可见。(详见第十八章和第十九章了解有关 CSS 属性动画的更多信息。)

提示

如果你想从不可见到可见实现淡入效果,请不要动画化visibility。而是动画化opacity

总结

虽然 CSS 格式化模型的某些方面一开始可能看起来不符合直觉,但随着你与它们的工作越来越多,它们开始变得合理起来。在许多情况下,看似荒谬甚至愚蠢的规则实际上存在是为了防止文档显示出怪异或不良的效果。块级元素在很多方面很容易理解,并且影响它们的布局通常是一个简单的任务。另一方面,行内元素可能更难管理,因为涉及的因素更多,其中之一是元素是否被替换或非替换。

第七章:填充、边框、轮廓和边距

在 第六章 中,我们讨论了元素显示的基础知识。在本章中,我们将看看你可以使用的 CSS 属性和值,以影响如何绘制元素框并使其相互分离。这些属性包括围绕元素的填充、边框和外边距,以及可能添加的任何轮廓。

基本元素框

如前一章所述,所有文档元素生成称为元素框的矩形框,描述元素在文档布局中占用的空间量。因此,每个框影响其他元素框的位置和大小。例如,如果文档中的第一个元素框高 1 英寸,那么下一个框将至少从文档顶部向下移动 1 英寸。如果将第一个元素框更改为高 2 英寸,每个后续元素框将向下移动 1 英寸,并且第二个元素框将至少从文档顶部向下移动 2 英寸。

默认情况下,视觉渲染的文档由许多不重叠的矩形框组成。如果手动定位或放置在网格上,框可以重叠,如果在正常流元素上使用负边距,可能会发生视觉重叠。

要理解外边距、填充和边框的处理方式,必须了解盒子模型,见 图 7-1。

css5 0701

图 7-1。CSS 盒子模型

图中的 图 7-1 故意省略了轮廓,原因在我们讨论轮廓时会清楚。

注意

内容区域的高度和宽度,以及块和行内方向上内容区域的尺寸,详见 第六章。如果您因高度、宽度、块轴和行内轴的讨论方式而觉得本章的其余部分有些困惑,请参考该章节进行详细解释。

填充

在元素的内容区域之外,我们发现其填充,位于内容和任何边框之间。设置填充的最简单方法是使用属性 padding

此属性接受任何长度值或百分比值。因此,如果您希望所有 <h2> 元素的四周都有 2 em 的填充,那就很容易实现(参见 图 7-2):

h2 {padding: 2em; background-color: silver;}

css5 0702

图 7-2。向元素添加填充

如 图 7-2 所示,默认情况下,元素的背景延伸到填充区域。如果背景是透明的,则设置填充会在元素内容周围创建额外的透明空间,但任何可见的背景都会延伸到填充区域(以及更远,正如稍后的部分所述)。

注意

可以通过使用属性 background-clip (参见 第八章)防止可见背景扩展到填充区域。

默认情况下,元素没有填充。例如,段落之间的分隔通常仅通过边距来实现(稍后您将看到)。另一方面,如果没有填充,元素的边框将非常接近元素本身的内容。因此,在给元素加边框时,通常也建议添加一些填充,就像 图 7-3 所示。

css5 0703

图 7-3. 填充对有边框的块级元素的影响

任何长度值都是允许的,从 em 到英寸。设置填充的最简单方法是使用单个长度值,该值等量应用于四个填充边。但是,有时您可能希望元素的每一边具有不同的填充量。如果您希望所有 <h1> 元素顶部填充为 10 像素,右侧填充为 20 像素,底部填充为 15 像素,左侧填充为 5 像素,您只需这样说:

h1 {padding: 10px 20px 15px 5px;}

值的顺序很重要,并遵循此模式:

padding: *`top` `right` `bottom` `left`*

记住此模式的一个好方法是牢记四个值顺时针围绕元素,从顶部开始。填充值总是按照这个顺序应用,因此为了获得想要的效果,您必须正确排列这些值。

记住边的正确顺序的一种简单方法,除了将其视为从顶部顺时针进行,还有助于避免“TRouBLe”——即 TRBL,即 top, right, bottom, left

此排序显示,像 padding 一样,像 heightwidth 一样,是一个物理属性:它指的是页面的物理方向,例如顶部或左侧,而不是基于书写方向。 (CSS 确实有书写模式填充属性,稍后您将看到。)

完全可以混合使用所使用的长度类型。在给定规则中,您不必局限于使用单个长度类型,而是可以根据元素的每一侧适当使用任何类型,如下所示:

h2 {padding: 14px 5em 0.1in 3ex;} /* value variety! */

图 7-4 显示了此声明的结果,附带一些额外的注释。

css5 0704

图 7-4. 混合值填充

复制值

有时您输入的值可能会有点重复:

p {padding: 0.25em 1em 0.25em 1em;}  /* TRBL - Top Right Bottom Left */

不过,您无需像这样一对一对地输入数字。与前述规则不同,尝试这样做:

p {padding: 0.25em 1em;}

这两个值足以代替四个。但是如何实现呢?CSS 定义了一些规则来适应 padding (以及许多其他简写属性)少于四个值的情况:

  • 如果 left 的值缺失,则使用为 right 提供的值。

  • 如果 bottom 的值也缺失,则使用为 top 提供的值。

  • 如果 右边 的值也缺失,则使用提供给 顶部 的值。

如果你更喜欢更视觉化的方法,请看图 7-5。

css5 0705

图 7-5. 值复制模式

换句话说,如果给定 padding 的三个值,第四个(左边)将从第二个(右边)复制。如果给定两个值,则第四个将从第二个复制,第三个(底部)从第一个(顶部)复制。最后,如果只给定一个值,则所有其他边将复制该值。

此机制允许你只提供必要的数值,如下所示:

h1 {padding: 0.25em 0 0.5em;} /* same as '0.25em 0 0.5em 0' */
h2 {padding: 0.15em 0.2em;}   /* same as '0.15em 0.2em 0.15em 0.2em' */
p {padding: 0.5em 10px;}      /* same as '0.5em 10px 0.5em 10px' */
p.close {padding: 0.1em;}     /* same as '0.1em 0.1em 0.1em 0.1em' */

该方法有一个小缺点,你最终肯定会遇到。假设你想要为 <h1> 元素设置顶部和左侧内边距为 10 像素,底部和右侧内边距为 20 像素。你需要写如下内容:

h1 {padding: 10px 20px 20px 10px;} /* can't be any shorter */

你能得到想要的效果,但是要把所有内容都理解清楚需要一些时间。不幸的是,在这种情况下没有办法减少所需数值的数量。让我们再举一个例子,假设你希望所有的内边距都是 0 —— 除了左内边距应为 3 em:

h2 {padding: 0 0 0 3em;}

使用内边距来分隔元素的内容区域可能比使用传统的外边距更棘手,尽管它并非没有其奖励。例如,要使用内边距保持段落之间传统的“一个空行”的间距,你需要写成这样:

p {margin: 0; padding: 0.5em 0;}

每个段落的半个字母的上下内边距靠在一起,并总计形成一个分隔。你为什么要这样做?因为这样你可以在段落之间插入分隔线,侧边框将接触以形成实线外观。以下代码定义了这些效果,如图 7-6 所示:

p {margin: 0; padding: 0.5em 0; border-bottom: 1px solid gray;
    border-left: 3px double black;}

css5 0706

图 7-6. 使用内边距而不是外边距

单边内边距

CSS 提供了一种方法来为元素的单个边设置内边距的值。实际上有四种方式。假设你想要将 <h2> 元素的左内边距设置为 3em。与其写出 padding: 0 0 0 3em,你可以采用这种方法:

h2 {padding-left: 3em;}

padding-left 选项是四个专门用于设置元素盒子四个边的内边距属性之一。它们的名称可能并不让人感到意外。

这些属性按照它们的名称的方式运作。例如,以下两条规则将产生相同量的内边距(假设没有其他 CSS):

h1 {padding: 0 0 0 0.25in;}
h2 {padding-left: 0.25in;}

类似地,这些规则将创建相等的内边距:

h1 {padding: 0.25in 0 0;}  /* left padding is copied from right padding */
h2 {padding-top: 0.25in;}

对于这个问题,这些规则同样适用:

h1 {padding: 0 0.25in;}
h2 {padding-right: 0.25in; padding-left: 0.25in;}

可以在单个规则中使用多个这些单边属性之一;例如:

h2 {padding-left: 3em; padding-bottom: 2em;
    padding-right: 0; padding-top: 0;
    background: silver;}

正如你在图 7-7 中所见,内边距被设置成我们想要的样子。在这种情况下,使用 padding 可能更容易,像这样:

h2 {padding: 0 0 2em 3em;}

css5 0707

图 7-7. 多个单边内边距

通常情况下,一旦您试图为多个边缘设置填充,使用速记 padding 更容易。但是从文档显示的角度来看,无论使用哪种方法都无关紧要,所以选择最简单的方法。

逻辑填充

正如本章节所示,物理属性有逻辑对应物,名称遵循一致的模式。对于 heightwidth,我们有 block-sizeinline-size。对于填充,我们有一组四个属性,对应于块方向和内联方向的起始填充和结束填充。它们被称为 逻辑属性,因为它们使用一些逻辑来确定应该应用到哪个物理边。

当您希望确保文本具有填充时,无论书写方向如何,这些属性都非常方便。例如,您可能希望稍微填充以使背景边缘远离每个块元素的起始和结束,并在每行文本的侧边填充更多。以下是实现此目的的一种方法,其结果显示在 图 7-8 中:

p {
     padding-block-start: 0.25em;
     padding-block-end: 0.25em;
     padding-inline-start: 1em;
     padding-inline-end: 1em;
}

css5 0708

图 7-8. 逻辑填充
警告

这些逻辑填充属性的百分比值始终相对于元素容器的 物理 宽度或高度进行计算,而不是其逻辑宽度或高度。因此,例如,当容器具有 width: 1000px 时,padding-inline-start: 10% 将计算为 100 像素,即使在垂直书写模式下也是如此。这可能会有所变化,但这是截至 2022 年末的一致(和规定)行为。

为单独元素的每个边缘显式声明填充值有点繁琐,两个速记属性可以帮助:一个用于块轴,另一个用于内联轴。

使用这些速记属性,您可以一次性设置块填充,另一次性设置内联填充。以下 CSS 与 “逻辑填充” 所示的效果相同:

p {
     padding-block: 0.25em;
     padding-inline: 1em;
}

每个属性都接受一个或两个值。如果有两个值,它们总是按 start end 的顺序。如果只有一个值,如前所示,那么将同一个值用于起始和结束的两个边缘。因此,要为元素设置 10 像素的块起始填充和 1 em 的块结束填充,可以这样写:

p {
     padding-block: 10px 1em;
}

不幸的是,逻辑填充没有更紧凑的速记形式——没有像 padding 那样接受四个值的 padding-logical。有关扩展 padding 属性以允许设置逻辑填充而不是物理填充的提案已被提出,但截至 2022 年末,这些提案尚未被采纳。截至本文撰写时,您能得到的最紧凑的逻辑填充是使用 padding-blockpadding-inline

百分比值和填充

我们可以为元素的填充设置百分比值。百分比是相对于父元素内容区域的宽度计算的,因此如果父元素的宽度以某种方式更改,则百分比也会更改。

例如,假设以下情况,如图 7-9 所示:

p {padding: 10%; background-color: silver;}
<div style="width: 600px;">
    <p>
        This paragraph is contained within a DIV that has a width of 600 pixels,
        so its padding will be 10% of the width of the paragraph's parent
        element. Given the declared width of 600 pixels, the padding will be 60
        pixels on all sides.
    </p>
</div>
<div style="width: 300px;">
    <p>
        This paragraph is contained within a DIV with a width of 300 pixels,
        so its padding will still be 10% of the width of the paragraph's parent.
        There will, therefore, be half as much padding on this paragraph as
        on the first paragraph.
    </p>
</div>

css5 0709

图 7-9. 填充、百分比和父元素的宽度

您可能已经注意到图 7-9 中段落的一些奇怪之处。它们的侧面填充不仅根据其父元素的宽度改变,顶部和底部填充也是如此。这是 CSS 中期望的行为。回顾属性定义,您会发现百分比值定义为相对于父元素的宽度。这也适用于顶部和底部填充以及左右填充。因此,给定以下样式和标记,段落的顶部填充将为 50 像素:

div p {padding-top: 10%;}
<div style="width: 500px;">
    <p>
        This is a paragraph, and its top margin is 10% the width of its parent
        element.
    </p>
</div>

如果所有这些看起来很奇怪,请考虑正常流中的大多数元素(正如我们所假设的那样)与包含其后代元素(包括填充)所需的高度一样高。如果一个元素的顶部和底部填充是父元素高度的百分比,则可能会导致无限循环,其中父元素的高度增加以容纳顶部和底部填充,然后必须增加以匹配新的高度,依此类推。

与忽略顶部和底部填充的百分比不同,规范的作者决定将其与父元素的内容区域宽度相关联,这不会根据其后代的宽度改变。这允许作者通过在所有四个边上使用相同的百分比来获得元素周围一致的填充。

相比之下,考虑没有声明宽度的元素。在这种情况下,元素框(包括填充)的整体宽度取决于父元素的宽度。这导致了流式页面的可能性,其中元素的填充根据父元素的实际大小放大或缩小。如果您设计一个文档,使其元素使用百分比填充,则当用户更改浏览器窗口的宽度时,填充将展开或收缩以适应。设计选择取决于您。

你也可以混合百分比和长度值。因此,要将<h2>元素的顶部和底部填充设定为半个 em,并将侧面填充设定为其父元素宽度的 10%,您可以声明如下,如图 7-10 所示:

h2 {padding: 0.5em 10%;}

css5 0710

图 7-10. 混合填充

在这里,尽管顶部和底部填充在任何情况下保持不变,但侧面填充将根据父元素的宽度而改变。

填充和内联元素

到目前为止,您可能已经注意到,讨论仅限于生成块级盒子的元素设置的填充。当填充应用于行内非替换元素时,效果略有不同。

假设您希望在强调的文本上设置顶部和底部填充:

strong {padding-top: 25px; padding-bottom: 50px;}

这在规范中是允许的,但是因为您将填充应用于行内非替换元素,所以对行高没有任何影响。当没有可见背景时,填充是透明的,因此前述声明在视觉上不会产生任何效果。这是因为行内非替换元素上的填充不会改变元素的行高。

要小心:带有背景颜色和填充的行内非替换元素可能会使背景在元素之上和之下延伸,如下所示:

strong {padding-top: 0.5em; background-color: silver;}

图 7-11 让您对可能的外观有了一个初步了解。

css5 0711

图 7-11. 行内非替换元素的顶部填充

行高不会改变,但由于背景颜色确实延伸到填充区域,每行的背景最终会重叠在之前的行上。这是预期的结果。

前述行为仅适用于行内非替换元素的顶部和底部;左侧和右侧则另当别论。我们将首先考虑单行内的小型行内非替换元素的情况。在这种情况下,如果您设置了左或右填充的值,它们将是可见的,就像图 7-12 明确表明的那样:

strong {padding-left: 25px; background: silver;}

css5 0712

图 7-12. 带有左填充的行内非替换元素

注意单词结束前的行内非替换元素和行内元素背景边缘之间的额外空间。如果需要,您可以在行内两端都添加这额外的空间:

strong {padding-left: 25px; padding-right: 25px; background: silver;}

正如预期的那样,图 7-13 显示了行内元素右侧和左侧有少许额外空间,上下没有额外空间。

css5 0713

图 7-13. 带有 25 像素边填充的行内非替换元素

现在,当行内非替换元素跨多行时,情况会有所变化。图 7-14 展示了行内非替换元素带填充在跨多行时的情况:

strong {padding: 0 25px; background: silver;}

左填充应用于元素的开头,右填充应用于元素的结尾。默认情况下,填充不会应用于每行的右侧和左侧。此外,您可以看到,如果没有填充,该行可能会在“background”后面而不是当前位置断开。padding 属性只通过改变元素内容在行内开始的位置来影响换行点。

css5 0714

图 7-14. 一个具有 25 像素侧填充的内联非替换元素跨两行文本显示
注意

可以通过属性box-decoration-break来改变每行框的结尾是否应用填充(或不应用填充)。更多细节请参见第六章。

填充和替换元素

替换元素也可以应用填充。对大多数人来说,最令人惊讶的情况是可以为图像应用填充,如下所示:

img {background: silver; padding: 1em;}

无论替换元素是块级还是内联,填充都会围绕其内容,并且背景颜色将填充到该填充区域,如图 7-15 所示。你还可以看到填充会将替换元素的边框(在本例中为虚线)从其内容推开。

css5 0715

图 7-15. 替换元素上的填充、边框和背景

现在,记住关于内联非替换元素的填充不影响文本行高度的所有内容?对于替换元素,你可以把这些内容全部抛掉,因为它们有不同的规则。正如你在图 7-16 中看到的那样,内联替换元素的填充确实会影响行的高度。

css5 0716

图 7-16. 填充内联替换元素

边框和外边距也是如此,您很快就会看到。

注意,如果图 7-16 中的图像未加载,或者某种方式设置为高度和宽度为 0,填充仍然会呈现在应该显示该元素的位置周围,即使该位置没有高度或宽度。

警告

截至 2022 年末,关于如何处理诸如<input>这样的表单元素的样式问题,如何处理仍存在不确定性。例如,复选框的填充位于何处并不完全清楚。因此,截至本文写作时,一些浏览器忽略了表单元素的填充(和其他形式的样式),而其他浏览器则尽其所能应用这些样式。

边框

超出元素填充的是其边框。元素的边框只是围绕内容和填充的一个或多个线条。默认情况下,元素的背景停止在外边框边缘处,因为背景不会延伸到外边距中,而边框刚好在外边距内部,因此被绘制“在”边框的下方。这在边框部分是透明的情况下很重要,例如虚线边框。

每个边框都有三个方面:它的宽度或厚度,它的样式或外观,以及它的颜色。边框的宽度的默认值是medium,在 2022 年明确声明为 3 像素宽。尽管如此,你通常看不到边框的原因是默认样式是none,它完全防止它们存在。(这种不存在还可以重设border-width的值,但我们稍后再讨论这个。)

最后,默认边框颜色是currentcolor,即元素本身的前景色。如果未为边框声明颜色,则它将与元素文本的颜色相同。另一方面,如果一个元素没有文本——比如说它只包含图像的表格——那么该表格的边框颜色将是其父元素的文本颜色(因为color是继承的)。因此,如果一个表格有边框,而<body>是它的父元素,根据这个规则

body {color: purple;}

然后,默认情况下,表格周围的边框将是紫色(假设用户代理未设置表格的颜色)。

CSS 规范将元素的背景区域定义为默认情况下延伸到边框的外缘。这很重要,因为一些边框是间歇性的——例如,dotteddashed边框——因此,元素的背景应出现在边框可见部分之间的空白处。

注意

可以通过使用属性background-clip来防止可见背景扩展到边框区域。详细信息请参见第八章。

带样式的边框

我们将从边框样式开始,这是边框的最重要的方面——不是因为它们控制边框的外观(尽管它们确实如此),而是因为没有样式,就根本没有边框。

CSS 为属性border-style定义了 10 种不同的样式,包括默认值none。图 7-17 展示了这些样式。此属性不被继承。

样式值hidden等同于none,但当应用于表格时,对边框冲突解决有稍微不同的影响。

css5 0717

图 7-17. 边框样式

至于double,它被定义为两条线加上它们之间的空间的宽度等于border-width的值(在下一节讨论)。然而,CSS 规范没有指定其中一条线应该比另一条线更粗,或者它们应该始终具有相同的宽度,或者空间应该比线条更粗或更细。所有这些选项都由用户代理决定,作者无法可靠地影响最终结果。

所有在图 7-17 中显示的边框都基于 graycolor 值,这使所有视觉效果更容易看到。边框样式如 insetoutsetgrooveridge 的外观总是以某种方式基于边框的颜色,尽管具体方法可能在用户代理之间有所不同。浏览器如何处理边框样式中的颜色可以并且确实有所不同。例如,图 7-18 展示了浏览器可能渲染内嵌边框的两种方式。

css5 0718

图 7-18. 渲染内嵌框的两种有效方式

在这个示例中,一个浏览器将 gray 值用于底部和右侧,顶部和左侧使用更深的灰色;另一个浏览器则使底部和右侧比 gray 更轻,而顶部和左侧更暗,但不及第一个浏览器那么深。

现在让我们为任何未访问的超链接内的图像定义一个边框样式。我们可能会将它们设为 outset,这样它们看起来像是一个“凸起的按钮”,如图 7-19 所示:

a:link img {border-style: outset;}

css5 0719

图 7-19. 对超链接图像应用突出边框

默认情况下,边框的颜色基于元素对 color 的值,在这种情况下,可能是 blue。这是因为图像包含在超链接中,超链接的前景色通常是 blue。如果你愿意,你可以将该颜色更改为 silver,就像这样:

a:link img {border-style: outset; color: silver;}

现在,边框将基于淡灰色的 silver,因为这是图像的前景色,即使图像实际上没有使用它,但它仍然传递给了边框。我们将在“边框颜色”中讨论另一种更改边框颜色的方法。

请记住,边框中的颜色变化是由用户代理决定的。让我们回到蓝色的突出边框,并在两个浏览器中进行比较,如图 7-20 所示。

再次注意到,一个浏览器会将颜色变浅或变深,而另一个浏览器只会使“阴影”边变暗,但不如第一个浏览器那么深。这就是为什么,如果需要特定的颜色集,作者通常会设置他们想要的确切颜色,而不是使用 outset 等边框样式并将结果留给浏览器。很快你会看到如何做到这一点。

css5 0720

图 7-20. 两个突出边框

多种样式

我们可以为给定边框定义多种样式。例如:

p.aside {border-style: solid dashed dotted solid;}

结果是一个段落,顶部边框是实线,右边边框是虚线,底部边框是点线,左边边框是实线。

再次看到了 TRBL 顺序的值,就像我们在设置多个值的 padding 时讨论过的那样。关于值复制的所有规则都适用于边框样式,就像它们适用于 padding 一样。因此,以下两个语句将产生相同的效果,如图 7-21 所示:

p.new1 {border-style: solid none dashed;}
p.new2 {border-style: solid none dashed none;}

css5 0721

图 7-21. 等效样式规则

单边样式

有时您可能希望仅为元素框的一个边设置边框样式,而不是四个边都设置。这就是单边边框样式属性发挥作用的地方。

单边边框样式属性相当易于理解。例如,如果您想要更改底部边框的样式,可以使用border-bottom-style

见到border与单边属性结合使用并不罕见。假设您希望在标题的三个边上设置实线边框,但左边没有边框,如图 7-22 所示。

css5 0722

图 7-22. 去除左边框

您可以通过两种方式来完成此操作,每种方式都与另一种等效:

h1 {border-style: solid solid solid none;}
/* the above is the same as the below */
h1 {border-style: solid; border-left-style: none;}

重要的是要记住,如果您要使用第二种方法,您必须在缩写之后放置单边属性之后,通常情况下是这样。这是因为border-style: solid实际上是声明border-style: solid solid solid solid。如果您将border-style-left: none放在border-style声明之前,那么缩写的值将覆盖none的单边值。

逻辑样式

如果您希望边框的样式与其在书写模式流中的位置有关,而不是固定在物理方向上,那么以下是适合您的边框样式属性。

padding-blockpadding-inline一样,border-block-styleborder-inline-style每个接受一个或两个值。如果给定两个值,则按start end的顺序取值。给定以下 CSS,您将得到类似于图 7-23 所示的结果:

p {border-block-style: solid double; border-inline-style: dashed dotted;}

css5 0723

图 7-23. 逻辑边框样式

您可以以以下更冗长的方式获得相同的结果:

p {
     border-block-start-style: solid;
     border-block-end-style: double;
     border-inline-start-style: dashed;
     border-inline-end-style: dotted;
}

这两种模式之间唯一的区别在于您需要输入的字符数,因此实际上,您可以自行选择使用哪种模式。

边框宽度

一旦您为边框分配了样式,下一步就是为其赋予一些宽度,最简单的方法是使用属性border-width或其兄弟属性之一。

每个这些属性用于设置特定边框边的宽度,就像与边距属性一样。

注意

截至 2023 年初,边框宽度仍然不能以百分比值给出,这实在有些遗憾。

有四种方法可以为边框分配宽度:您可以给它一个长度值,例如4px0.1em,或者使用三个关键字中的一个。这些关键字是thinmedium(默认值)和thick。根据规范,thick是 5px,比medium的 3px 宽,比 1px 的thin更宽——这是有道理的。

图 7-24 展示了这三个关键词以及它们如何相互关联以及与它们所围绕的内容之间的关系。

css5 0724

图 7-24. 边框宽度关键词之间的关系

假设一个段落有背景颜色和边框样式设置:

p {background-color: silver;
    border-style: solid;}

默认情况下,边框的宽度是medium。我们可以很容易地改变它:

p {background-color: silver;
    border-style: solid; border-width: thick;}

边框宽度可以被设置为非常夸张的极端值,比如设置 1000 像素的边框,尽管这很少是必要的(或明智的)。重要的是要记住,边框和因此border-width的值参与框模型,影响元素的大小。

可以使用两种熟悉的方法为各个边设置宽度。第一种方法是使用本节开头提到的特定属性之一,例如border-bottom-width。另一种方法是在border-width中使用值复制,遵循通常的 TRBL 模式,这在图 7-25 中有说明:

h1 {border-style: dotted; border-width: thin 0px;}
p {border-style: solid; border-width: 15px 2px 8px 5px;}

css5 0725

图 7-25。值复制和不均匀边框宽度

逻辑边框宽度

话虽如此,如果希望根据书写方向设置边框宽度,可以使用与物理属性配对的逻辑对应项。

正如你看到的边框宽度,它们可以单独设置每一边,也可以压缩成border-block-widthborder-inline-width属性。以下两条规则将产生完全相同的效果:

p {
     border-block-width: thick thin;
     border-inline-width: 1em 5px;
}
p {
     border-inline-start-width: 1em;
     border-inline-end-width: 5px;
     border-block-start-width: thick;
     border-block-end-width: thin;
}

没有任何边框

到目前为止,我们只讨论了使用可见的边框样式,比如solidoutset。让我们考虑一下当你将border-style设置为none时会发生什么:

p {border-style: none; border-width: 20px;}

尽管边框的宽度是20px,但样式被设置为none。在这种情况下,不仅边框的样式消失了,它的宽度也消失了。边框就这样不存在了。为什么?

正如您可能还记得的那样,在本章早些时候使用的术语表明,样式为none的边框不存在。这些话语被非常谨慎地选择,因为它们有助于解释这里发生的情况。由于边框不存在,它就不能有任何宽度,所以无论你如何定义,宽度都自动设置为0(零)。

毕竟,如果一个饮用杯是空的,你无法真正描述它是半满无物的。只有在杯子里真的有内容时,才能讨论杯子内容的深度。同样地,只有在边框存在的情况下,才有讨论边框宽度的意义。

这很重要要记住,因为忘记声明边框样式是一个常见的错误。这会导致开发者各种沮丧,因为乍一看,样式看起来是正确的。不过,根据以下规则,没有任何<h1>元素会有任何类型的边框,更不用说宽度为 20 像素的边框了:

h1 {border-width: 20px;}

由于border-style的默认值是none,不声明样式与声明border-style: none完全相同。因此,如果要显示边框,必须声明边框样式。

边框颜色

与边框的其他方面相比,设置颜色非常容易。CSS 使用物理简写属性border-color,可以一次接受多达四个颜色值。(请参见“颜色”以了解颜色的有效值格式。)

如果提供的值少于四个,则通常会进行值复制。因此,如果希望<h1>元素具有上下边框为细灰色,侧边框为粗绿色,并且<p>元素周围具有中灰色边框,以下样式就足够了,并显示在图 7-26 中:

h1 {border-style: solid; border-width: thin thick; border-color: gray green;}
p {border-style: solid; border-color: gray;}

css5 0726

图 7-26. 边框有多个方面

一个单一的color值将应用于四个边框,就像前面示例中的段落一样。另一方面,如果提供四个颜色值,可以使每个边框颜色不同。可以使用任何类型的颜色值,从命名颜色到十六进制和 HSL 值:

p {border-style: solid; border-width: thick;
    border-color: black hsl(0 0% 25% / 0.5) #808080 silver;}

如果不声明颜色,默认为currentcolor,即元素的前景色。因此,以下声明将按图 7-27 所示显示:

p.shade1 {border-style: solid; border-width: thick; color: gray;}
p.shade2 {border-style: solid; border-width: thick; color: gray;
    border-color: black;}

css5 0727

图 7-27. 边框颜色基于元素的前景和border-color属性的值

结果是第一个段落具有灰色边框,因为使用了段落的前景色。然而,第二个段落具有黑色边框,因为使用了border-color显式指定的颜色。

物理单边边框颜色属性也存在。它们的工作方式与边框样式和宽度的单边属性类似。一种为标题添加实线黑色边框和实线灰色右边框的方法如下:

h1 {border-style: solid; border-color: black; border-right-color: gray;}

逻辑边框颜色

与边框样式和宽度一样,逻辑属性会覆盖物理属性:两个简写,四个长手写。

因此,以下两条规则将产生完全相同的结果:

p {
     border-block-color: black green;
     border-inline-color: orange blue;
}
p {
     border-inline-start-width: orange;
     border-inline-end-width: blue;
     border-block-start-width: black;
     border-block-end-width: green;
}

透明边框

正如您可能还记得的那样,如果边框没有样式,则没有宽度。然而,在某些情况下,您可能希望创建一个看起来没有边框但实际上有宽度的边框。这就是transparent边框颜色值的用武之地。

假设我们希望一组三个链接在默认情况下具有不可见的边框,但当链接悬停时,边框看起来是凹陷的。我们可以通过在非悬停情况下将边框设为透明来实现这一点:

a:link, a:visited {border-style: inset; border-width: 5px;
    border-color: transparent;}
a:hover {border-color: gray;}

这将产生图 7-28 中显示的效果。

在某种意义上,transparent使您可以像使用额外的填充一样使用边框。如果要使它们可见,空间会被保留,防止在添加可见边框时内容重新排列。

css5 0728

图 7-28. 使用透明边框

单边简写边框属性

缩写属性(例如border-colorborder-style)并不总是像你想象的那样有用。例如,你可能希望将厚、灰色、实线边框应用于所有<h1>元素,但仅限于底部。如果仅使用到目前为止讨论过的属性,将会很难实现这样的边框效果。以下是两个示例:

h1 {border-bottom-width: thick;  /* option #1 */
    border-bottom-style: solid;
    border-bottom-color: gray;}
h1 {border-width: 0 0 thick;    /* option #2 */
    border-style: none none solid;
    border-color: gray;}

鉴于涉及到的所有输入,这两者都不是很方便。幸运的是,有更好的解决方案可用:

h1 {border-bottom: thick solid rgb(50% 40% 75%);}

这将仅将值应用于底部边框,如图 7-29 所示,其他边框将保持默认状态。由于默认的边框样式是none,所以元素的其他三个边框不会显示出来。

css5 0729

Figure 7-29. 使用缩写属性设置底部边框

正如你可能已经猜到的那样,CSS 具有四个物理缩写属性和四个逻辑缩写属性。

我们可以利用这些属性创建一些复杂的边框,例如图 7-30 中显示的那些:

h1 {border-left: 3px solid gray;
    border-right: green 0.25em dotted;
    border-top: thick goldenrod inset;
    border-bottom: double rgb(13% 33% 53%) 10px;}

css5 0730

Figure 7-30. 非常复杂的边框

如你所见,实际值的顺序并不重要。以下三条规则将产生完全相同的边框效果:

h1 {border-bottom: 3px solid gray;}
h2 {border-bottom: solid gray 3px;}
h3 {border-bottom: 3px gray solid;}

你也可以省略一些值,让它们的默认值生效,如下所示:

h3 {color: gray; border-bottom: 3px solid;}

由于未声明边框颜色,所以默认值(currentcolor)会被应用。请记住,如果省略了边框样式,那么none的默认值将导致你的边框不存在。

相反,如果仅设置样式,你仍然会得到一个边框。假设你想要一个dashed的顶部边框样式,并且愿意让宽度默认为medium,颜色与元素文本的颜色相同。在这种情况下,只需以下标记(如图 7-31 所示):

p.roof {border-top: dashed;}

css5 0731

Figure 7-31. Dashing across the top of an element

还要注意的是,由于每个边框边属性仅适用于特定的一侧,所以不存在值复制的可能性——这是毫无意义的。每种类型的值只能有一个:即只有一个宽度值,只有一个颜色值,以及只有一个边框样式。因此,不要尝试声明多个值类型:

h3 {border-top: thin thick solid purple;} /* two width values--WRONG */

整个声明都是无效的,用户代理会忽略它。

全局边框

现在,我们来讨论最简短的缩写边框属性:border,它会对元素的四个边框均产生影响。

这种属性的优势在于非常紧凑,尽管这种简洁性会引入一些限制。在我们担心这些之前,让我们看看border是如何工作的。如果你希望所有<h1>元素都有厚厚的银色边框,下面的声明将显示如图 7-32 所示:

h1 {border: thick silver solid;}

css5 0732

Figure 7-32. 一个非常简短的边框声明

使用border的缺点在于你只能定义单一的全局样式、宽度和颜色。你为border提供的值会对所有四个边都生效。如果你希望某个边有不同的边框样式,请使用其他的边框属性。然而,再次利用级联特性也是可能的:

h1 {border: thick goldenrod solid;
    border-left-width: 20px;}

第二条规则将第一条规则分配的左边框宽度值thick覆盖为20px,如你在图 7-33 中所见。

css5 0733

图 7-33. 利用级联优势

你仍然需要谨慎处理简写属性:如果你省略了一个值,系统将自动填充默认值,这可能会产生意想不到的效果。考虑以下情况:

h4 {border: medium green;}

在这里,我们未能分配border-style,这意味着将使用默认值none,因此没有任何<h4>元素会有边框。

边框与内联元素

处理边框和内联元素应该听起来非常熟悉,因为规则与我们之前讨论的填充和内联元素的规则基本相同。尽管如此,我们还是会简要地再次触及这个话题。

首先,无论你在内联元素上有多厚的边框,元素的行高都不会改变。让我们在加粗文本上设置块起始和块结束的边框:

strong {border-block-start: 10px solid hsl(216,50%,50%);
        border-block-end: 5px solid #AEA010;}

如前所述,在块的起始和结束添加边框对行高没有任何影响。然而,由于边框是可见的,它们会被绘制出来,如在图 7-34 所示。

css5 0734

图 7-34. 内联非替换元素上的边框

边框必须有一个位置。这就是它们所在的位置。如果需要,它们会被绘制在前一行文本的上方,并在下一行文本的下方。

同样,这一切只适用于内联元素的块起始和块结束两侧;内联两侧则不同。如果你在内联一侧应用边框,它们不仅会可见,还会使周围的文本产生位移,就像你在图 7-35 中看到的那样。

strong {border-inline-start: 25px double hsl(216 50% 50%); background: silver;}

css5 0735

图 7-35. 内联非替换元素具有内联起始边框

与填充一样,边框对于浏览器在处理行断开时的计算没有直接影响。唯一的影响是边框所占用的空间可能会稍微移动行的某些部分,这可能会改变行尾的单词。

注意

边框的绘制方式(或不绘制方式)在每个行框的结束处可以通过box-decoration-break属性进行修改。详细信息请参见第 6 章。

另一方面,对于像图像这样的替换元素,效果与填充类似:边框影响文本行的高度,并将文本向两侧移动。因此,假设以下样式,我们得到类似于图 7-36 中看到的结果:

img {border: 1em solid rgb(216,108,54);}

css5 0736

图 7-36. 行内替换元素上的边框

圆角边框

我们可以通过使用属性border-radius来定义圆角距离(或两个),软化元素边框的方形角和整个背景区域。在这种特定情况下,我们将从简写物理属性开始,然后在本节末尾提到各个物理属性,之后我们将检查逻辑等效项。

圆角边框角的半径是圆形或椭圆的半径,四分之一用于定义边框的曲线路径。我们将从圆形开始,因为它们稍微容易理解。

假设我们想要使元素的角显然是圆角。以下是一种方法:

#example {border-radius: 2em;}

结果显示在图 7-37,其中圆形图表已添加到两个角落(所有四个角落都采用相同的四舍五入)。

css5 0737

图 7-37. 边框半径如何计算

聚焦于左上角。在那里,边框开始曲线,在边框顶部以下 2 em 处,距左侧边框 2 em 处开始。曲线沿着 2 em 半径圆的外侧进行。

如果我们绘制一个仅包含曲线部分的左上角的框,那个框将是 2 em 宽和 2 em 高。底部右边角也是如此。

使用单一长度值,我们得到圆形的角度形状。如果使用单一百分比,结果则更椭圆。例如,考虑以下内容,图 7-38 中有所说明:

#example {border-radius: 33%;}

css5 0738

图 7-38. 百分比边框半径如何计算

再次聚焦于左上角。在左边缘上,边框曲线从元素框顶部到底部的高度的 33%处开始。换句话说,如果元素框从顶部边框到底部边框的高度为 100 像素,则曲线将从元素框顶部 33 像素处开始。

同样地,在顶部边缘上,曲线从元素框左边缘的宽度的 33%处开始。因此,如果框宽(比如)为 600 像素,曲线将从左边缘开始的 198 像素处开始,因为 600 × 0.33 = 198。

在这两点之间的曲线形状与水平半径为 198 像素、垂直半径为 33 像素的椭圆的左上边缘相同。(这与水平轴为 396 像素、垂直轴为 66 像素的椭圆相同。)

在每个角落都做同样的事情,形成一组相互镜像而非相同的角形状。

border-radius 提供单个长度或百分比值意味着所有四个角都会具有相同的圆角形状。正如你可能在语法定义中看到的那样,你可以为 border-radius 提供最多四个值。因为 border-radius 是一个物理属性,这些值按顺时针顺序从左上到左下排列,如下所示:

#example {border-radius:
     1em  /* Top Left */
     2em  /* Top Right */
     3em  /* Bottom Right */
     4em; /* Bottom Left */
}

TL-TR-BR-BL 这个顺序可以用“TiLTeR BuRBLe”来记忆,如果你倾向于这样的东西的话。重要的是,圆角从左上角开始,顺时针方向开始。

如果省略一个值,则使用类似于 padding 的模式填充缺失的值,依此类推。如果有三个值,第四个值从第二个值复制。如果有两个值,则第三个值从第一个值复制,第四个值从第二个值复制。如果只有一个值,则缺少的三个值从第一个值复制。因此,以下两条规则是相同的,并将产生 图 7-39 中显示的结果:

#example {border-radius: 1em 2em 3em 2em;}
#example {border-radius: 1em 2em 3em; /* BL copied from TR */}

css5 0739

图 7-39. 不同圆角的变化

图 7-39 有一个重要的方面:内容区域背景的圆角化与其他背景一起。看看银色的曲线和句点在外面?当内容区域的背景与填充背景不同时(你将在 第 8 章 中看到如何做到这一点),并且圆角足够大以影响内容和填充之间的边界时,这是预期的行为。

因为 border-radius 改变了元素的边框和背景的绘制方式,但改变元素框的形状。考虑一下 图 7-40 所描述的情况。

css5 0740

图 7-40. 圆角元素仍然是框

在这里,我们有一个浮动到左侧的元素,并且其他文本流经它。圆角边框完全圆形,使用 border-radius: 50% 在一个正方形元素上。一些文本从圆角边框外伸出。超出圆角边框后,页面背景可见,圆角 本来 应该在那里的地方。

一眼看去,你可能会认为该元素已经从盒子形状变成了圆形(技术上是椭圆),而文本恰好伸出来。但是看看流过浮动的文本。它并没有流入到圆角“留下的”区域。那是因为浮动元素的角落仍然存在。只是由于 border-radius 的存在,它们的边框和背景没有填充进去。

圆角夹紧

如果半径值太大,会溢出到其他角落会发生什么?例如,对于 border-radius: 100% 或者在元素上使用 border-radius: 9999px,而该元素远未达到 10000 像素的高度或宽度呢?

在任何这种情况下,圆角都会“夹紧”到元素的每个象限的最大可能值。确保按钮始终看起来像圆角形状可以通过以下方式实现:

.button {border-radius: 9999em;}

这将仅将元素最短的两端(通常是左右侧,但不能保证)封顶为平滑的半圆形。

更复杂的角落形状

现在你已经了解到如何将单个半径值分配给角落以形成它的形状,让我们讨论一下当角落获得两个值时会发生什么,更重要的是,它们如何获得这些值。

例如,假设我们希望将角落水平方向圆角化为 3 个字符单位,垂直方向为 1 个字符单位。我们不能简单地使用 border-radius: 3ch 1ch,因为这样会使得左上角和右下角分别圆角化为 3ch,而其他两个角则分别为 1ch。插入一个斜杠将会得到我们想要的效果:

#example {border-radius: 3ch / 1ch;}

这等效于以下表述:

#example {border-radius: 3ch 3ch 3ch 3ch / 1ch 1ch 1ch 1ch;}

这种语法的工作方式是,给出每个角落圆角化椭圆的水平半径,然后在斜杠后给出每个角落的垂直半径。在这两种情况下,值是按照 TiLTeR BuRBLe 的顺序给出的。

这是一个更简单的例子,如图 7-41 所示:

#example {border-radius: 1em / 2em;}

css5 0741

图 7-41。椭圆形角落圆角化

每个角落沿水平轴为 1em,沿垂直轴为 2em 进行圆角化,这种方式你在前面的章节中已经详细看到了。

这是一个稍微复杂一点的版本,提供了斜杠两侧的两个长度,如图 7-42 所示:

#example {border-radius: 2.5em 2em / 1.5em 3em;}

css5 0742

图 7-42。不同椭圆形圆角计算

在这种情况下,左上角和右下角沿水平轴曲度为 2.5em,沿垂直轴为 1.5em。而右上角和左下角则分别沿水平轴为 2em,沿垂直轴为 3em。

记住,在斜杠前使用水平值,斜杠后使用垂直值。例如,如果我们想让左上角和右下角的圆角水平方向为 1em,垂直方向也为 1em(即圆形的圆角),则数值应该写成这样:

#example {border-radius: 1em 2em / 1em 3em;}

百分比在这里也是适用的。如果我们希望将元素的角落圆角,使其侧面完全圆角,但仅水平延伸到元素的 2 个字符单位,我们会这样写:

#example {border-radius: 2ch / 50%;}

角落混合

到目前为止,我们圆角的角落都相当简单 —— 总是相同的宽度、样式和颜色。但情况并非总是如此。如果一个粗的红色实线边框圆角化成一个细的绿色虚线边框会发生什么?

规范指示,在涉及宽度从较粗的边框过渡到较细的边框时,边框的宽度应在圆角的曲线上逐渐收缩。

当涉及到不同的样式和颜色时,规范对如何实现这一点并不那么明确。请考虑图 7-43 中展示的各种样本。

css5 0743

图 7-43. 圆角近距离查看

第一个是简单的圆角,颜色、宽度或样式没有变化。第二个显示了从一种厚度到另一种厚度的圆角。你可以将第二种情况视为在外边缘定义一个圆形形状,在内边缘定义一个椭圆形状。

在第三种情况中,颜色和厚度保持不变,但是角落从左侧的实线样式到顶部的双线样式。样式之间的过渡突然,在曲线的中点发生。

第四个示例展示了从厚实线过渡到较细双线的情况。请注意过渡点的位置,这是在中点。相反,它是通过取两个边框厚度的比率来确定的,并使用该比率找到过渡点。假设左边框厚度为 10 像素,顶部边框厚度为 5 像素。通过将两者相加得到 15 像素,左边框获得 2/3(10/15),顶部边框获得 1/3(5/15)。因此,左边框的样式在曲线的 2/3 处使用,顶部边框的样式在曲线的 1/3 处使用。宽度仍然在圆角的长度范围内平滑变化。

第五和第六个示例展示了在混合颜色时会发生什么。实际上,颜色与样式保持链接。这种颜色之间的硬切换是 2022 年后浏览器的常见行为,但未来可能不会一直如此。规范明确表示用户代理可能通过使用线性渐变从一种边框颜色过渡到另一种边框颜色。也许某天会实现,但目前,变换是突然的。

第七个示例在图 7-43 中展示了一个我们尚未讨论的情况:“如果边框等于或比border-radius的值更厚会发生什么?”在这种情况下,角的外侧是圆角的,但内侧不是,如图所示。这将会在以下代码中发生:

#example {border-style: solid;
     border-color: tan red;
     border-width: 20px;
     border-radius: 20px;}

各自的圆角属性

在介绍完border-radius之后,你可能想知道是否可以一次只圆角一个角。是的,可以!首先,让我们考虑物理角落,这是border-radius所汇集的地方。

每个属性都为其角设置了曲线形状,不影响其他角。有趣的是,如果你提供两个值,一个是水平半径,一个是垂直半径,它们之间没有斜杠。真的。这意味着以下两条规则在功能上是等效的:

#example {border-radius:
     1.5em 2vw 20% 0.67ch / 2rem 1.2vmin 1cm 10%;
     }
#example {
     border-top-left-radius: 1.5em 2rem;
     border-top-right-radius: 2vw 1.2vmin;
     border-bottom-right-radius: 20% 1cm;
     border-bottom-left-radius: 0.67ch 10%;
}

单个角边框半径属性主要用于设置公共角圆角,然后仅覆盖一个。因此,类似漫画书般的对话气泡形状可以如下所示完成,其结果显示在图 7-44 中:

.tabs {border-radius: 2em;
     border-bottom-left-radius: 0;}

css5 0744

图 7-44. 形状如对话气泡的链接

除了物理角落外,CSS 还有逻辑角落。

你可能会想:“等等,其他逻辑属性看起来不是这样!”是的,这些确实有些不同。这是因为如果我们有一个像border-block-start-radius这样的属性,它将应用于沿块起始边的两个角。但是如果你还有border-inline-start-radius,它将适用于沿行内起始边的两个角,其中一个也在块起始边上。

所以逻辑边框半径属性的工作方式是按照border-block-inline-radius模式标记的。因此,border-start-end-radius设置了位于块起始和行内结束边缘交界处的角的半径。看下面的例子,它在图 7-45 中有所说明:

p {border-start-end-radius: 2em;}

css5 0745

图 7-45. 圆角化块起始,行内结束角

请记住,你可以使用与之前在border-top-left-radius及其伙伴部分中显示的椭圆形角半径定义相同的空格分隔值模式。但是,该值仍然符合水平半径然后垂直半径的模式,而不是相对于块和行内流方向。这似乎是 CSS 中的一个小疏忽,但截至 2022 年末它是现实。

要记住的一件事是,正如你所见,角形状会影响元素的背景和(可能)填充和内容区域,但不会影响任何图像边框。等等,图像边框?那是什么?很高兴你问!

图像边框

各种边框样式已经足够好了,但仍然相当有限。如果你想要在一些元素周围创建一个真正复杂、视觉丰富的边框,该怎么办?以前,我们会创建复杂的多行表格来实现这种效果,但由于图像边框的出现,你几乎可以创造出各种类型的边框,没有几乎没有限制。

载入和切片边框图像

如果你要使用图像来创建图像的边框,你需要定义它或者从某处获取它。border-image-source属性告诉浏览器去哪里查找它。

让我们加载一个单个圆形图像作为边框图像,并使用以下样式,其结果如图 7-46 所示:

border: 25px solid;
border-image-source: url(i/circle.png);

css5 0746

图 7-46. 定义边框图像的来源

这里有几个要点需要注意。首先,如果没有声明border: 25px solid,就根本不会有边框。记住,如果border-style的值为none,那么边框的宽度就是 0。因此,要使边框图像出现,你需要有一个边框,这意味着声明一个border-style值,而不是nonehidden。它不一定要是solid。其次,border-width的值确定了边框图像的实际宽度。如果没有声明值,它将默认为medium,即 3 像素。如果边框图像加载失败,边框就是border-color的值。

好的,我们设置了一个宽度为 25 像素的边框区域,然后将图像应用到了上面。这给了我们每个角落相同的圆形。但为什么它只出现在那里,而不沿着边缘呢?答案在于物理属性border-image-slice的定义方式。

border-image-slice的作用是建立一组覆盖在图像上的四条切片线,它们的位置决定了图像在图像边框中如何被切片使用。该属性最多可以接受四个值,依次定义从顶部、右侧、底部和左侧边缘的偏移量。是的,这里又有了那个 TRBL 模式,将border-image-slice作为物理属性固定了下来。并且值的复制在这里也生效了,因此一个值将被用于所有四个偏移量。图 7-47 展示了一些基于百分比的偏移模式的小样本。

css5 0747

图 7-47. 不同的切片模式
注意

截至 2022 年末,border-image-slice没有逻辑属性的等效物。如果提议的logical关键字或类似的内容被采纳并实施,将可以在写入流相关的方式中使用border-image-slice。也没有单侧属性;也就是说,没有border-left-image-slice这样的东西。

现在让我们取一个有着 3×3 个圆形的网格的图像,并将其切片用于图像边框。图 7-48 展示了这个图像的单个副本和结果图像边框:

border: 25px solid;
border-image-source: url(i/circles.png);
border-image-slice: 33.33%;

糟糕!这…有点有趣。边缘的拉伸性是默认行为,这在某种程度上是合理的,正如你将看到的(以及如何改变)在“改变重复模式”中。除了这种效果外,你可以在图 7-48 中看到,切片线恰好位于圆圈之间,因为所有圆圈的大小都相同,所以 1/3 的偏移量将切片线放置在它们之间。角圆圈进入边框的角落,每边的圆圈被拉伸以填充其边缘。

css5 0748

图 7-48. 全景图像边框

(“等等,中间的灰色圆圈怎么了?”你可能会想。这是一个有趣的问题!现在,只需将其接受为生活中的一个小谜团,尽管这是一个稍后在本节中会解释的谜团。)

好吧,那么为什么我们在本节开始时的第一个边框图像示例中,只在边框区域的角落放置了图像,而不是完全环绕它?

每当切片线相遇或超过彼此时,角落图像会被创建,但边缘图像会变为空白。这在border-image-slice: 50%的情况下最容易可视化,此时图像被切成四个象限,每个角落有一个,边缘没有剩余部分。

然而,任何值超过50%都会产生相同的基本结果,即使图像不再被切成整齐的四分之一。因此,对于border-image-slice: 100%——这是默认值——每个角落都得到整个图像,而边缘则为空。这种效果的几个示例显示在图 7-49 中。

这就是为什么当我们想要环绕边框区域、角落和边缘时,我们必须有一个 3 × 3 的圆圈网格。

css5 0749

图 7-49. 各种阻止边缘切片的图案

除了使用百分比偏移外,我们还可以通过数字来定义偏移量。不是长度,正如你可能会认为的那样,而是一个纯数字。在像 PNG 或 JPEG 这样的光栅图像中,该数字与图像上的像素一一对应。如果你有一个光栅图像,并想要为切片线定义 25 像素的偏移量,这就是如何做到的,如图 7-50 所示:

border: 25px solid;
border-image-source: url(i/circles.png);
border-image-slice: 25;

css5 0750

图 7-50. 数字切片

糟糕!再次发生了!问题在于光栅图像是 150 × 150 像素,因此每个圆圈都是 50 × 50 像素。然而,我们的偏移量只有25,即 25 像素。所以切片线被放置在图像上,如图 7-51 所示。

这开始让我们了解为什么边缘图像的默认行为是拉伸它们。请注意,角落如何流入边缘,从视觉上讲。

如果您将图像更改为大小不同的图像,则数值偏移不会适应新的大小,而百分比会适应。关于数字偏移的有趣之处在于,它们在非光栅图像(如 SVG)上的工作效果与在光栅图像上的工作效果相同。所以也是百分比。一般来说,最好在可能的情况下使用百分比作为切片偏移量,即使这意味着进行一些数学运算以获得完全正确的百分比。

css5 0751

图 7-51. 切片线在 25 像素处

现在让我们来解决图像中心的奇特情况。在前面的示例中,一个圆圈位于 3 × 3 的圆圈网格的中心,但当图像应用于边框时,它会消失。事实上,在前面的例子中,不仅中间的圆圈消失了,整个中心切片也消失了。这种中心切片的丢弃是图像切片的默认行为,但您可以通过在border-image-slice值的末尾添加fill关键字来覆盖它。如果我们在前面的例子中添加fill,如下所示,我们将得到图 7-52 所示的结果:

border: 25px solid;
border-image-source: url(i/circles.png);
border-image-slice: 25 fill;

这是中心切片,填充元素的背景区域。事实上,它覆盖了元素可能具有的任何背景,包括任何背景图像或颜色,因此您可以将其用作背景的替代品或作为其附加项。

css5 0752

图 7-52. 使用填充切片

您可能已经注意到,所有我们的边框区域的宽度都是一致的(通常为25px)。这并不一定是实际情况,无论图像边框实际如何切割。假设我们采用一直在使用的圆圈边框图像,将其分为三等分,但使边框宽度不同:

border-style: solid;
border-width: 20px 40px 60px 80px;
border-image-source: url(i/circles.png);
border-image-slice: 50;

这将产生类似于图 7-53 所示的结果。尽管切片线本质上设置为 50 像素(通过50),但生成的切片被调整大小以适应其占据的边框区域。

css5 0753

图 7-53. 不均匀的边框图像宽度

改变图像的宽度

到目前为止,我们所有的图像边框都依赖于border-width值来设置边框区域的大小,边框图像精确填充了这些区域。也就是说,如果顶部边框的高度为 25 像素,则填充它的边框图像将为 25 像素。如果您希望使图像的大小与由border-width定义的区域大小不同,可以使用物理属性border-image-width

关于border-image-width的基本事实是,它与border-image-slice非常相似,只是border-image-width切片本身的边框框。

要理解这意味着什么,让我们从长度值开始。我们将设置 1 em 边框宽度如下:

border-image-width: 1em;

这将使切片线向内推 1 em,从边框区域的每一侧显示,如图 7-54 所示。

css5 0754

图 7-54. 放置边框图像宽度的切片线

因此,顶部和底部边框区域高度为 1 em,右侧和左侧边框区域宽度为 1 em,每个角落的高度和宽度也为 1 em。鉴于此,使用 border-image-slice 创建的边框图像将填充到这些边框区域中,其方式由 border-image-repeat 规定(我们马上会讨论)。因此,在 Figure 7-55 中,我们即使将 border-width 设置为 0,也能让边框图像显示出来,通过使用 border-image-width。如果边框图像加载失败,这很有用,但不希望边框变得像图像边框那样厚。你可以使用类似这样的东西:

border: 2px solid;
border-image-source: url(stars.gif);
border-image-width: 12px;
border-image-slice: 33.3333%;
padding: 12px;

css5 0755

图 7-55. 带和不带边框图像的边框

如果没有边框图片可用,这允许将一个 12 像素的星星边框替换为一个 2 像素的实线边框。请记住,如果边框图片确实加载了,你需要留出足够的空间让它显示出来,避免与内容重叠(默认情况下)。在下一节中,你将看到如何解决这个问题。

现在我们已经确定了宽度切片线的放置方式,处理百分比值的方式应该是有意义的,只要记住偏移量是相对于整体边框框而言,而不是每个边框边缘。例如,请考虑以下声明,在 Figure 7-56 中有所说明:

border-image-width: 33%;

css5 0756

图 7-56. 百分比切片线的放置

与长度单位一样,这些线条与边框框的各自边缘有偏移。它们行进的距离是相对于边框框的。一个常见的错误是假设百分比值是相对于由 border-width 定义的边框区域;也就是说,给定 border-width 值为 30pxborder-image-width: 33.333%; 的结果将是 10 像素。但事实并非如此!它是沿着该轴的整体边框框的三分之一。

border-image-width 的行为与 border-image-slice 不同之处在于它如何处理互相重叠的片段,比如在以下情况下:

border-image-width: 75%;

正如你可能记得的那样,对于 border-image-slice,如果片段互相通过,那么侧边区域(顶部、右侧、底部和/或左侧)将被清空。对于 border-image-width,数值将被按比例减少直至它们不再互相通过。因此,给定 75% 的前值,浏览器会将其视为 50%。类似地,后面两个声明将得到相同的结果:

border-image-width: 25% 80% 25% 40%;
border-image-width: 25% 66.6667% 25% 33.3333%;

请注意,在这两个声明中,右偏移量是左值的两倍。这就是所谓的按比例减少数值直至它们不再重叠的含义:换句话说,直至它们不再总和超过 100%。如果上下重叠,同样的方式也会被应用。

当涉及到border-image-width的数字值时,情况变得更加有趣。如果设置border-image-width: 1,边框图像区域将由border-width的值确定。这是默认行为。因此,以下两个声明将产生相同的结果:

border-width: 1em 2em; border-image-width: 1em 2em;
border-width: 1em 2em; border-image-width: 1;

您可以增加或减少数字值,以获得border-width定义的边框区域的某个倍数。Figure 7-57 展示了一些示例。

在每种情况下,数字都已乘以边框区域的宽度或高度,生成的值指示偏移量从相关边缘处放置的内部距离。因此,对于border-top-width设置为 3 像素的元素,border-image-width: 10将在元素顶部创建一个 30 像素的偏移量。将border-image-width更改为0.333,顶部偏移量将是一个像素。

css5 0757

图 7-57. 各种数字边框图像宽度

最后一个值,auto,非常有趣,因为其结果取决于另外两个属性的状态。如果作者显式定义了border-image-source,那么border-image-width: auto将使用由border-image-slice得出的值。否则,它将使用由border-width得出的值。这两个声明将产生相同的结果:

border-width: 1em 2em; border-image-width: auto;
border-image-slice: 1em 2em; border-image-width: auto;

请注意,您可以混合使用border-image-width的值类型。以下都是有效的,并且在实时网页中尝试将会非常有趣:

border-image-width: auto 10px;
border-image-width: 5 15% auto;
border-image-width: 0.42em 13% 3.14 auto;
注意

border-image-slice一样,截至 2022 年末,尚不存在border-image-width的逻辑属性等效项。

创建边框悬挂

好了,现在我们可以定义这些大图像片段和宽度了,但如何防止它们重叠内容呢?我们可以添加大量填充,但如果图像加载失败或浏览器不支持边框图像,则会留下大量空间。处理这种情况是物理属性border-image-outset的用途所在。

无论您使用长度还是数字,border-image-outset都会将边框图像区域向外推,超出边框框盒,在某种程度上类似于切片线的偏移。区别在于这里的偏移是向外而不是向内。就像border-image-width一样,border-image-outset的数字值是由border-width定义的宽度的倍数,而不是border-image-width

注意

border-image-sliceborder-image-width一样,截至 2022 年末,尚不存在border-image-outset的逻辑属性等效项。

要了解这将如何有助于,想象一下我们想要使用边框图像,但如果图像不可用,则使用一个薄实线边框的备用。我们可能会这样开始:

border: 2px solid;
padding: 0.5em;
border-image-slice: 10;
border-image-width: 1;

在这种情况下,我们有半个 em 的填充;在默认的浏览器设置中,这大约是 8 像素。再加上 2 像素的实线边框,从内容边缘到外边框边缘的距离为 10 像素。因此,如果边框图像可用且已呈现,它将不仅填充边框区域,而且还会填充填充区域,直到与内容紧密相连。

我们可以增加填充来解决这个问题,但如果图像没有出现,那么在内容和细边框之间就会有大量多余的填充。因此,让我们把边框图像向外推,如下所示:

border: 2px solid;
padding: 0.5em;
border-image-slice: 10;
border-image-width: 1;
border-image-outset: 8px;

这在图 7-58 中有详细说明,并且与无外扩边框图像相比较。

css5 0758

图 7-58. 创建图像边框悬垂

在第一种情况下,图像边框被推得足够远,以至于不仅重叠了填充区域,而且实际上重叠了边距区域!我们还可以分割差异,使图像边框大致居中于边框区域,如下所示:

border: 2px solid;
padding: 0.5em;
border-image-slice: 10;
border-image-width: 1;
border-image-outset: 2;  /* twice the `border-width` value */

需要注意的是,如果将图像边框拉得太远,以至于重叠其他内容或被浏览器窗口边缘裁剪(或两者兼有),就必须留意。如果是这样,图像边框将会被绘制在前一个元素的内容和背景之间,从而隐藏背景,但如果后续内容有背景或边框,则会部分遮挡。

改变重复模式

到目前为止,你已经看到了很多在示例的边缘拉伸的图像。在某些情况下,这种拉伸可能很方便,但在其他情况下可能是真正的眼中钉。通过物理属性border-image-repeat,你可以改变这些边缘的处理方式。

注意

与先前的边框图像属性一样,截至 2022 年底,border-image-repeat没有逻辑属性的等效项。

让我们看看这些值的作用,然后依次讨论每一个。你已经看到了stretch,所以效果很熟悉。每个边都会得到一个单独的图像,被拉伸以匹配边框侧区域的高度和宽度,填充图像正在填充的区域。

repeat值会平铺图像,直到填满其边框侧区域的所有空间。确切的排列方式是将图像居中放置在其边框盒中,然后从该点向外平铺图像的副本,直到边框侧区域填满。这可能导致一些重复的图像在边框区域的边缘被裁剪,正如在图 7-59 中所见。

css5 0759

图 7-59. 各种图像重复模式

round值略有不同。使用此值时,浏览器将边框侧区域的长度除以图像在其中重复的尺寸。然后四舍五入到最接近的整数,并重复该数量的图像。此外,它会拉伸或压缩图像,使它们在重复时紧密接触。

例如,假设顶部边框侧面区域宽度为 420 像素,平铺的图像宽度为 50 像素。将 420 除以 50 得到 8.4,因此四舍五入为 8。因此,将有八个图像平铺。但是,每个图像都会拉伸到 52.5 像素宽(420 ÷ 8 = 52.5)。类似地,如果右侧边框侧面区域高度为 280 像素,50 像素高的图像将被平铺六次(280 ÷ 50 = 5.6,四舍五入为 6),每个图像将被压缩到 46.6667 像素高(280 ÷ 6 = 46.6667)。如果你仔细看 图 7-59,你可以看到顶部和底部的圆圈有些拉伸,而右侧和左侧的圆圈显示出一些压缩。最后一个值 space,开始时类似于 round,即将边框侧面区域的长度除以平铺图像的大小,然后四舍五入。不同之处在于,得到的数字总是向下取整,并且图像不会被扭曲,而是均匀分布在边框区域内。

因此,如果给定顶部边框侧面区域为 420 像素宽,并且要平铺的图像宽度为 50 像素,则仍然有 8 个图像需要重复(8.4 四舍五入为 8)。这些图像将占据 400 像素的空间,剩下 20 像素。这 20 像素被 8 除,得到 2.5 像素。每个图像的两侧各占 1.25 像素的空间。这样每个图像之间就有 2.5 像素的间隙,并且第一个图像之前和最后一个图像之后各有 1.25 像素的空间(参见 图 7-60 中的 space 重复示例)。

css5 0760

图 7-60. 各种空间重复

简写边框图像

对于边框图像的单一简写物理属性(毫不奇怪地)是 border-image。它的书写方式有些不同寻常,但是在不多打字的情况下提供了很大的功能。

必须承认,这个属性值的语法有些不寻常。为了获取所有不同的片段、宽度和偏移的属性,并且能够区分哪个是哪个,决定将它们用斜线符号(/)分隔,并要求按特定顺序列出:片段、宽度,然后是偏移。图像源和重复值可以放在这三个值链之外的任何地方。因此,以下规则是等效的:

.example {
    border-image-source: url(eagles.png);
    border-image-slice: 40% 30% 20% fill;
    border-image-width: 10px 7px;
    border-image-outset: 5px;
    border-image-repeat: space;
}
.example {border-image: url(eagles.png) 40% 30% 20% fill / 10px 7px / 5px space;}
.example {border-image: url(eagles.png) space 40% 30% 20% fill / 10px 7px / 5px;}
.example {border-image: space 40% 30% 20% fill / 10px 7px / 5px url(eagles.png);}

简写明显减少了输入量,但一目了然的清晰度也降低了。

和大多数简写属性一样,如果省略了任何一个单独的部分,那么将会提供默认值。例如,如果我们只提供了一个图像源,则其余属性将设置为它们的默认值。因此,以下两个声明将产生完全相同的效果:

border-image: url(orbit.svg);
border-image: url(orbit.svg) stretch 100% / 1 / 0;

一些例子

边框图像在概念上可能难以内化,因此值得看一些使用它们的示例。

首先,让我们设置一个具有凹角和凸起外观的边框,就像一个匾额,同时也提供一个类似颜色的简单外凸边框的后备方案。我们可能会使用类似这样的样式和一张图片,如图 7-61 所示,以及最终结果和后备结果:

#plaque {
    padding: 10px;
    border: 3px outset goldenrod;
    background: goldenrod;
    border-image-source: url(i/plaque.png);
    border-image-repeat: stretch;
    border-image-slice: 20 fill;
    border-image-width: 12px;
    border-image-outset: 9px;
}

css5 0761

图 7-61. 一个简单的匾额效果及其旧版浏览器的后备方案

注意,侧边的切片被完美设置为可拉伸的一部分 —— 它们沿拉伸轴只是重复的彩色条带。在这种情况下,它们也可以是重复的或圆角的,但拉伸效果已经很好了。而且,由于这是默认值,我们本可以完全省略border-image-repeat声明。

接下来,让我们尝试创建一些海洋风格的东西:一个图像边框,其周围有波浪起伏。由于我们事先不知道元素的宽度或高度,并且希望波浪可以从一个流到另一个,我们将使用round来利用其缩放行为,同时尽可能容纳更多的波浪。您可以在图 7-62 中看到结果,以及用于创建效果的图像:

#oceanic {
    border: 2px solid blue;
    border-image:
        url(waves.png) 50 fill / 20px / 10px round;
}

css5 0762

图 7-62. 一个波浪边框

在此,请注意一种可能的问题,即如果添加了元素背景会发生什么。为了澄清情况,我们将为该元素添加一个红色背景,结果如图 7-63 所示:

#oceanic {
    background: red;
    border: 2px solid blue;
    border-image:
        url(waves.png) 50 fill / 20px / 10px round;
}

看到波浪之间可见的背景颜色了吗?这是因为波浪图像是带有透明部分的 PNG 图像,以及图像切片宽度和外推使得部分背景区域可以通过边框的透明部分看到。这可能是一个问题,因为在某些情况下,您可能希望在图像无法显示时使用背景颜色作为后备方案。通常,这是一个最好通过不需要后备情况的背景、使用border-image-outset将图像拉出到足够远,以至于背景区域的任何部分都不可见,或者使用background-clip: padding-box(见“裁剪背景”)来解决的问题。

如您所见,边框图像具有很大的威力。请务必明智地使用它们。

css5 0763

图 7-63. 通过图像边框可见的背景区域

轮廓

CSS 定义了一种特殊的元素装饰,称为轮廓。在实践中,轮廓通常绘制在边框的外侧,尽管(正如您将看到的)这并不是全部。正如规范所说,轮廓与边框在三个基本方面有所不同:

  • 轮廓是可见的,但不占据布局空间。

  • 用户代理通常在:focus状态下渲染元素的轮廓,这正是因为它们不占据布局空间,因此不会改变布局。

  • 轮廓可以是非矩形的。

我们还将添加第四个:

  • 轮廓是一种全或无的选择:您不能独立地为边框的一侧设置样式。

让我们开始准确了解所有这些意味着什么。首先,我们将逐一比较各种属性,将它们与其边框相关的对应物进行比较。

轮廓样式

border-style类似,您可以为轮廓设置样式。实际上,这些值对于以前设置过边框样式的人来说应该是熟悉的。

两个主要的区别是,轮廓不能有hidden样式,如边框可以有;而轮廓可以有auto样式。这种样式允许用户代理对轮廓的外观进行额外处理,正如 CSS 规范中所解释的:

auto值允许用户代理渲染自定义的轮廓样式,通常是平台的用户界面默认样式,或者可能比 CSS 中详细描述的样式更丰富的样式,例如,带有半透明外边缘像素的圆角轮廓,看起来像是发光的。

另外一点是,auto允许浏览器为不同的元素使用不同的轮廓;例如,超链接的轮廓可能与表单输入的轮廓不同。使用auto时,outline-width的值可能会被忽略。

除了这些差异之外,轮廓具有与边框相同的所有样式,如图 7-64 所示。

较不明显的差异在于,与border-style不同,outline-style一种简写属性。你不能用它为每条边的轮廓设置不同的样式,因为轮廓不能以这种方式进行样式化。没有outline-top-style这样的东西。这对于所有其他轮廓属性也是如此。由于outline-style的这一方面,一个属性同时适用于物理和逻辑布局需求。

css5 0764

图 7-64. 各种轮廓样式

轮廓宽度

一旦您决定轮廓的样式(假设样式不是none),您可以为轮廓定义一个宽度。

关于轮廓宽度,我们已经说过的与边框宽度相同的内容很少。如果轮廓样式为none,则轮廓的宽度设置为0thickmedium宽,mediumthin宽,但规范没有为这些关键字定义确切的宽度。图 7-65 展示了几种轮廓宽度。

css5 0765

图 7-65. 各种轮廓宽度

与以往一样,真正的区别在于outline-width不是一种简写属性,并且满足物理和逻辑布局需求。您只能为整个轮廓设置一个宽度,并且不能为不同的边设置不同的宽度。(这些原因很快就会变得明确。)

轮廓颜色

您的轮廓是否有样式和宽度?太好了!让我们为它添加一些颜色!

这与border-color几乎相同,但有一个警告,即它是一种全有或全无的命题——例如,没有outline-left-color

唯一的主要差异是默认值invertinvert的预期操作是对大纲可见部分的所有像素执行“颜色转换”。颜色反转的优势在于,它可以使大纲在各种情况下显著突出,无论背景如何。

然而,截至 2022 年底,真正没有浏览器引擎支持invert。(有一段时间一些浏览器支持,但后来取消了支持。)考虑到这一点,如果使用invert,浏览器将拒绝它,并且将使用颜色关键字currentcolor代替。(详见“颜色关键字”。)

唯一的大纲简写

到目前为止,您已经看到了三个看起来像简写属性但实际上不是的大纲属性。现在是唯一一个真正的简写属性:outline

可能不足为奇,就像border一样,这是设置大纲的整体样式、宽度和颜色的便捷方式。图 7-66 展示了各种大纲。

css5 0766

图 7-66. 各种大纲

到目前为止,大纲看起来非常像边框。那么它们有什么不同呢?

它们的不同之处

边框和大纲之间的第一个主要区别是,就像外凸边框图像一样,大纲根本不影响布局。以任何方式。它们纯粹是表现性的。

要理解这意味着什么,请考虑以下样式,图 7-67 中有所说明:

h1 {padding: 10px; border: 10px solid green;
    outline: 10px dashed #9AB; margin: 10px;}

css5 0767

图 7-67. 大纲覆盖页边距

看起来很正常,对吧?您看不到的是大纲完全覆盖了页边距。如果我们放置一条虚线来显示页边缘,它们将沿着大纲的外边缘运行。(我们将在下一节处理页边距。)

这就是所谓的大纲不影响布局。让我们考虑另一个例子,这次是两个<span>元素被赋予了大纲。您可以在图 7-68 中看到结果:

span {outline: 1em solid rgba(0,128,0,0.5);}
span + span {outline: 0.5em double purple;}

css5 0768

图 7-68. 重叠的大纲

大纲不影响行高,但它们也不会把<span>们推向一边。文本的布局就好像大纲根本不存在一样。

这提出了大纲的一个更有趣的特征:它们不总是矩形的,也不总是连续的。考虑应用于跨越两行的<strong>元素的此大纲,如在图 7-69 中的两种情况所示:

strong {outline: 2px dotted gray;}

css5 0769

图 7-69. 不连续和非矩形的大纲

第一个案例有两个完整的轮廓框,每个碎片都有一个。在第二种情况下,由于较长的<strong>元素使得两个碎片堆叠在一起,轮廓“融合”成一个包围碎片的单个多边形。你不会发现边框做那样

这就是为什么 CSS 没有像outline-right-style这样的特定于侧面的轮廓属性:如果轮廓变成非矩形,哪些侧面是正确的?

警告

截至 2022 年底,不是每个浏览器都将内联碎片合并成单一的连续多边形。在不支持此行为的浏览器中,每个碎片仍然是一个自包含的矩形,就像图 7-69 中的第一个例子一样。此外,Firefox 和 Chrome 根据border-radius圆角处理轮廓,而 Safari 则保持角落为矩形。

边距

大多数正常流元素之间的间隔是由于元素的边距。设置边距会在元素周围创建额外的空白空间。空白空间通常指其他元素无法存在的区域,父元素的背景可见。图 7-70 显示了两个没有任何边距的段落与具有边距的相同两个段落之间的差异。

css5 0770

图 7-70. 带有和不带有边距的段落

最简单设置边距的方法是使用物理属性margin

假设您希望在<h1>元素上设置四分之一英寸的边距(已添加背景颜色,以便清楚地看到内容区域的边缘):

h1 {margin: 0.25in; background-color: silver;}

这将在<h1>元素的每一侧设置四分之一英寸的空白空间,如图 7-71 所示。这里,虚线表示边距的外边缘,但这些线条仅用于说明,在 Web 浏览器中实际上不会显示。

css5 0771

图 7-71. 为<h1>元素设置边距

margin属性可以接受任何长度的测量单位,无论是像素、英寸、毫米还是 em。然而,margin的默认值实际上是0,因此如果您不声明一个值,通常不会出现边距。

然而,在实际操作中,浏览器通常为许多元素预先分配样式,边距也不例外。例如,在启用 CSS 的浏览器中,边距会在每个段落元素的上方和下方生成“空白行”。因此,如果您不为<p>元素声明边距,浏览器可能会自行应用一些边距。无论您声明什么,都会覆盖默认样式。

最后,可以为margin设置百分比值。有关此值类型的详细信息,请参阅“百分比和边距”。

长度值和边距

任何长度值都可以用于设置元素的边距。例如,很容易为段落元素应用 10 像素的空白。以下规则为段落元素设置了银色背景、10 像素的填充和 10 像素的边距:

p {background-color: silver; padding: 10px; margin: 10px;}

这将在每个段落的每一侧外边框边缘之外增加 10 像素的空间。您同样可以使用margin为图像周围设置额外的空间。比如说,您想要在所有图像周围留出 1 em 的空间:

img {margin: 1em;}

就是这么简单。

有时,您可能希望在元素的每一边都有不同的空间量。这也很容易,多亏了我们之前使用过的值复制行为。如果您想让所有<h1>元素的顶部边距为 10 像素,右侧边距为 20 像素,底部边距为 15 像素,左侧边距为 5 像素,那么只需这样做:

h1 {margin: 10px 20px 15px 5px;}

您也可以混合使用不同类型的长度值。在给定的规则中,您不受限于使用单一的长度类型,如以下所示:

h2 {margin: 14px 5em 0.1in 3ex;} /* value variety! */

图 7-72 显示了这个声明的结果,附带一些额外的注释。

css5 0772

图 7-72. 混合值边距

百分比和边距

我们可以为元素的边距设置百分比值。与填充一样,百分比边距值是相对于父元素内容区域的宽度计算的,因此如果父元素的宽度以某种方式改变,它们也会相应变化。例如,假设以下情况,这在 图 7-73 中有说明:

p {margin: 10%;}
<div style="width: 200px; border: 1px dotted;">
    <p>
        This paragraph is contained within a DIV that has a width of 200 pixels,
        so its margin will be 10% of the width of the paragraph's parent (the
        DIV). Given the declared width of 200 pixels, the margin will be 20
        pixels on all sides.
    </p>
</div>
<div style="width: 100px; border: 1px dotted;">
    <p>
        This paragraph is contained within a DIV with a width of 100 pixels,
        so its margin will still be 10% of the width of the paragraph's
        parent. There will, therefore, be half as much margin on this paragraph
        as on the first paragraph.
    </p>
</div>

css5 0773

图 7-73. 父元素宽度和百分比

请注意,顶部和底部边距与右侧和左侧边距保持一致;换句话说,顶部和底部边距的百分比是相对于元素的宽度计算的,而不是其高度。您之前也看到过这种情况——在 “填充” 中,如果您记不清楚,现在再看一遍,以了解它的运作方式。

单边边距属性

正如您在本章中看到的那样,CSS 有一些属性可以让您在盒子的单侧设置边距,而不影响其他侧。有四个物理边属性、四个逻辑边属性和两个逻辑的简写属性。

这些属性的运作方式如您所预期的那样。例如,以下两个规则将给出相同数量的边距:

h2 {margin: 0 0 0 0.25in;}
h2 {margin: 0; margin-left: 0.25in;}

类似地,以下两个规则将有相同的结果:

h2 {
     margin-block-start: 0.25in;
     margin-block-end: 0.5em;
     margin-inline-start: 0;
     margin-inline-end: 0;
}
h2 {margin-block: 0.25in 0.5em; margin-inline: 0;}

边距合并

在块级盒子的块起始和块结束边距上有一个有趣且经常被忽视的方面是,在正常流布局中它们会合并。这是两个(或多个)沿着块轴交互的边距合并成交互边距中最大的一个的过程。

其中一个经典例子就是段落之间的空间。通常,可以使用以下规则设置该空间:

p {margin: 1em 0;}

这将使每个段落的块起始和块末端边距都设置为1em。如果边距折叠,那么每个段落之间将有 2 个 em 的间距。但实际上只有 1 个 em;这两个边距一起折叠了。

为了更清楚地说明这一点,让我们回到百分比边距的示例。这一次,我们将添加虚线来显示边距的位置,如图 7-74 所示。

css5 0774

图 7-74。边距折叠

该示例显示了两个段落内容之间的分隔距离。这是 60 像素,因为这是两者交互的较宽边距。第二个段落的块起始边距 30 像素被折叠,留下了第一个段落的块末端边距控制整体。

所以在某种意义上,图 7-74 是错误的:如果你严格按照 CSS 规范来看,第二段落的块起始(顶部)边距实际上会被重置为 0。它不会伸入第一个段落的块末端边距,因为一旦发生边距折叠,它就不存在了。尽管如此,最终结果是相同的。

边距折叠还解释了当一个元素位于另一个元素内部时出现的一些奇怪情况。考虑以下样式和标记:

header {background: goldenrod;}
h1 {margin: 1em;}

<header>
    <h1>Welcome to ConHugeCo</h1>
</header>

<h1> 上的边距会将 header 的边缘推开,远离 <h1> 的内容,对吗?嗯,并非完全如此。请参见图 7-75。

发生了什么?内联边距生效了——从文本被移动的方式我们可以看到——但块起始和块末端边距消失了!

它们并没有消失。它们只是从header元素中突出出来,并与header元素的块起始边距(零宽度)发生了交互。虚线在图 7-76 中展示了发生了什么。

css5 0775

图 7-75。父元素内边距折叠

css5 0776

图 7-76。父元素内边距折叠,显示

那里的块轴边距——推开了任何可能位于<header>元素之前或之后的内容,但没有推开<header>本身的边缘。这是预期的结果,即使这通常不是期望的结果。至于为什么是预期的结果,想象一下如果你把段落放在列表项中会发生什么。如果没有指定的边距折叠行为,段落的块起始边距(在这种情况下是顶部)将把它向下推,使其与列表项的标志(或编号)严重不对齐。

注意

父元素上的填充和边框等因素可以中断边距的折叠。有关更多详细信息,请参见“块轴边距的折叠”中的讨论。

负边距

可以为元素设置负边距。这可能导致元素的框突出其父元素或重叠其他元素。考虑以下规则,这些规则在图 7-77 中有所说明:

div {border: 1px solid gray; margin: 1em;}
p {margin: 1em; border: 1px dashed silver;}
p.one {margin: 0 -1em;}
p.two {margin: -1em 0;}

css5 0777

图 7-77. 负边距的实际应用

在第一种情况下,数学计算结果显示,段落的计算宽度加上其内联开始和内联结束边距恰好等于父元素 <div> 的宽度。因此,段落最终比父元素宽出 2 ems。

在第二种情况中,负的块开始和块结束边距会将段落的块开始和块结束外边缘向内移动,这就是它最终重叠在它之前和之后的段落上的原因。

结合负边距和正边距实际上非常有用。例如,你可以通过巧妙地使用正负边距使段落从父元素“突出”,或者可以通过几个重叠或随机放置的框创建蒙德里安效果,如图 7-78 所示:

div {background: hsl(42,80%,80%); border: 1px solid;}
p {margin: 1em;}
p.punch {background: white; margin: 1em -1px 1em 25%;
  border: 1px solid; border-right: none; text-align: center;}
p.mond {background: rgba(5,5,5,0.5); color: white; margin: 1em 3em -3em -3em;}

由于 mond 段落的负底边距,其父元素的底部被向上拉动,使段落能够突出其父元素的底部。

css5 0778

图 7-78. 从父元素中突出

边距和内联元素

边距也可以应用于内联元素。假设你想在强调文本的块开始和块结束上设置边距:

strong {margin-block-start: 25px; margin-block-end: 50px;}

这在规范中是允许的,但在内联非替换元素上,它们对行高没有任何影响(与填充和边框一样)。由于边距总是透明的,你甚至看不到它们的存在。实际上,它们根本没有任何效果。

与填充类似,当你将边距应用于内联非替换元素的内联开始和内联结束两侧时,布局效果会有所改变,如图 7-79 所示:

strong {margin-inline-start: 25px; background: silver;}

css5 0779

图 7-79. 具有内联开始边距的内联非替换元素

请注意,单词的末尾与内联非替换元素的背景边缘之间存在额外空间。如果你希望,可以在内联元素的两端都添加这种额外空间:

strong {margin: 25px; background: silver;}

如预期,图 7-80 显示内联元素的内联开始和内联结束两侧有一些额外空间,但上下没有额外空间。

css5 0780

图 7-80. 具有 25 像素边距的内联非替换元素

现在,当内联非替换元素跨越多行时,情况就不同了。图 7-81 展示了当具有边距的内联非替换元素跨越多行文本时会发生什么:

strong {margin: 25px; background: silver;}

css5 0781

图 7-81. 具有 25 像素侧边距的行内非替换元素显示在两行文本中

行内起始边距应用于元素的开头,行内结束边距应用于元素的末尾。边距不应用于每个行片段的行内起始和行内结束侧。此外,您可以看到,如果没有边距,该行可能会更早地断开一个或两个单词。边距通过改变元素内容在行内开始的点来影响断行。

小贴士

通过使用 box-decoration-break 属性,您可以改变边框装饰如何(或不)应用于每个行框的末端。详见 第六章。

当我们对行内非替换元素应用负边距时,情况变得更加有趣。元素的块起始和块结束不受影响,行高也不受影响,但元素的行内起始和行内结束可能会重叠其他内容,如图 7-82 图 所示:

strong {margin: -25px; background: silver;}

css5 0782

图 7-82. 具有负边距的行内非替换元素

替换的行内元素代表另一个故事:为它们设置的边距确实会影响行的高度,无论是增加还是减少,这取决于块起始和块结束边距的值。行内替换元素的行内侧边距与非替换元素的行内侧边距的行为相同。图 7-83 图 展示了对行内替换元素设置边距所产生的一系列布局效果。

css5 0783

图 7-83. 具有不同边距值的行内替换元素

概要

可以对任何元素应用边距、边框和填充,这样可以详细管理元素之间的间隔和外观。理解它们如何相互作用是 Web 设计的基础。

第八章:背景

默认情况下,元素的背景区域包括内容框、填充框和边框框,边框绘制在背景之上。(您可以在本章中使用 CSS 来改变这一点。)

CSS 允许您将一个实心不透明或半透明颜色应用到元素的背景上,并且可以将一个或多个图像应用到单个元素的背景上,甚至描述各种形状的自定义颜色渐变以填充背景区域。

设置背景颜色

要声明元素背景的颜色,您可以使用属性background-color,它接受任何有效的颜色值。

如果您希望颜色从元素的内容区域稍微延伸出来,请添加一些填充,如下面的代码所示,并在图 8-1 中有所说明:

p {background-color: #AEA;}
p.padded {padding: 1em;}
<p>A paragraph.</p>
<p class="padded">A padded paragraph.</p>

css5 0801

图 8-1. 背景颜色和填充

您可以为任何元素设置背景颜色,从<body>一直到内联元素如<em><a>background-color的值不会被继承。

它的默认值是关键字transparent,这应该是有道理的:如果一个元素没有定义的颜色,它的背景应该是透明的,这样它的祖先元素的背景和内容就会可见。

想象一下这意味着什么的一种方式是想象一个透明的塑料标志安装在纹理墙上。墙壁仍然通过标志可见,但这不是标志的背景;这是墙壁的背景(以 CSS 术语来说)。类似地,如果您设置页面画布具有背景,它可以通过文档中所有没有自己背景的元素来看到。

它们不继承背景;它通过元素可见。这可能看起来是一个无关紧要的区别,但当我们讨论背景图像时,这是一个关键的区别。

显式设置透明背景

大多数情况下,您不会使用关键字transparent,因为这是默认值。不过,偶尔使用它也是有用的。

想象一下,您必须包含的第三方脚本已经将所有图像设置为白色背景,但是您的设计包含一些透明 PNG 图像,您不希望这些图像的背景是白色的。为了确保您的设计选择优先,您应该声明以下内容:

img.myDesign {background-color: transparent;}

没有这一点(并给您的图像添加类),您的半透明图像将不会显示为半透明;相反,它们会看起来像是有固定白色背景。

在半透明图像上选择正确的背景色虽然好,但文字与背景颜色之间的良好对比是必须的。如果文字与任何背景部分的对比不足够大,文字将变得难以辨认。始终确保文字与背景之间的对比度大于或等于小文字的 4.5:1,大文字的 3:1。

在根元素上同时声明颜色和背景颜色,并确保良好的对比度,通常被认为是一个良好的实践。当声明颜色而没有声明背景颜色时,CSS 验证器将生成警告,例如,“您没有在您的color旁边添加了background-color”,以提醒您作者-用户颜色交互可能会发生,并且您的规则没有考虑到这种可能性。警告并不意味着您的样式无效:只有错误才会阻止验证。

背景和颜色组合

通过结合 colorbackground-color,你可以创建有趣的效果:

h1 {color: white; background-color: rgb(20% 20% 20%);
    font-family: Arial, sans-serif;}

图 8-2 描述了这个例子。

css5 0802

图 8-2. <h1> 元素的反向文本效果

有多少种颜色组合就有多少种颜色,我们无法在这里展示所有的组合。尽管如此,我们仍会尝试为你展示一些你可以做的事情。

如 图 8-3 所示,这个样式表稍微复杂一些:

body {color: black; background-color: white;}
h1, h2 {color: yellow; background-color: rgb(0 51 0);}
p {color: #555;}
a:link {color: black; background-color: silver;}
a:visited {color: gray; background-color: white;}

css5 0803

图 8-3. 更复杂样式表的结果

还有一个问题是当你为替换元素应用背景时会发生什么。我们已经讨论了带有透明部分的图像,例如 PNG 或 WebP。假设你想为 JPEG 创建一个双色调边框。你可以通过为图像添加背景颜色和一点内边距来实现,如下面的代码所示,并在 图 8-4 中进行了说明:

img.twotone {background-color: red; padding: 5px; border: 5px solid gold;}

css5 0804

图 8-4. 使用背景和边框来双色调图像

技术上,背景延伸到外边框的边缘,但由于边框是实心且连续的,我们看不到其背后的背景。5 像素的内边距允许在图像和其边框之间看到一圈细微的背景,创造出“内边框”的视觉效果。这种技术可以扩展到使用盒阴影(在章节末讨论)和背景图像如渐变(在 第九章 讨论)创建更复杂的效果。

裁剪背景

当你为替换元素(如图像)应用背景时,背景将透过任何透明部分显示出来。默认情况下,背景颜色延伸到元素边框的外边缘,在边框本身是透明的时候显示在边框后面,或者当边框样式为 dotteddasheddouble 时,显示在边框之间的空白区域。

要防止背景显示在半透明或完全透明的边框后面,我们可以使用 background-clip。该属性定义了元素背景的延伸范围。

默认值 border-box 指示背景绘制区域(即 background-clip 定义的内容)延伸到边框的外边缘。有了这个值,如果存在边框的话,背景将始终在可见部分的边框后面绘制。

如果选择值 padding-box,背景将仅延伸到填充区域的外边缘(也是边框的内边缘)。因此,背景不会绘制在边框后面。另一方面,值 content-box 限制背景只到元素的内容区域。

这三个值的效果在 图 8-5 中有所说明,这是以下代码的结果:

div[id] {color: navy; background: silver;
         padding: 1em; border: 0.5em dashed;}
#ex01 {background-clip: border-box;}  /* default value */
#ex02 {background-clip: padding-box;}
#ex03 {background-clip: content-box;}

css5 0805

图 8-5. 三种基于框的背景剪裁类型

看起来很简单,但是存在几个注意事项。首先,background-clip 对根元素没有影响(在 HTML 中,这可以是 <html> 元素,或者如果没有定义 <html> 的背景样式,则是 <body> 元素)。这与如何处理根元素的背景绘制有关。

第二,如果元素具有圆角,背景区域的确切剪裁可以减少,这要归功于 border-radius 属性(参见 第七章)。这基本上是常识,因为如果给元素添加了显著的圆角,希望背景被这些圆角剪裁而不是伸出圆角外。可以这样理解,背景绘制区域由 background-clip 决定,然后任何需要进一步剪裁的圆角都会得到适当的剪裁。

第三,background-clip 的值可能与一些更有趣的 background-repeat 值交互作用不良,我们稍后会讲到。

第四,background-clip 定义了背景的剪裁区域。它不影响其他背景属性。对于纯色背景来说,这是一个没有意义的区别;但是对于我们接下来要讨论的背景图片来说,它可能产生很大的影响。

还有一个值,text,它将背景剪切到元素的文本中。换句话说,文本被背景“填充”,而元素的其余背景区域保持透明。这是通过“填充”元素的文本来添加纹理的简单方法。

关键在于,要看到这种效果,必须去除元素的前景色。否则,前景色会遮挡背景。考虑以下内容,其结果显示在 图 8-6 中:

div {color: rgb(255,0,0); background: rgb(0,0,255);
     padding: 0 1em; margin: 1.5em 1em; border: 0.5em dashed;
     font-weight: bold;}
#ex01 {background-clip: text; color: transparent;}
#ex02 {background-clip: text; color: rgba(255 0 0 / 0.5);}
#ex03 {background-clip: text;}

css5 0806

图 8-6。将背景剪切到文本

对于第一个例子,前景色被完全设置为透明,蓝色背景仅在与元素内容中的文本形状相交的地方可见。在段落内部的图像中不可见,因为图像的前景色不能设置为transparent

在第二个例子中,前景色已设置为rgba(255 0 0 0.5),这是半透明的红色。那里的文本呈紫色,因为半透明的红色与其下的蓝色混合。边框则将其半透明的红色与其后面的白色背景混合,得到淡红色。

在第三个例子中,前景色是纯粹的不透明红色。文本和边框都是完全红色,没有任何蓝色背景的迹象。在这种情况下看不见它,因为它已被裁剪到文本中。前景完全遮挡了背景。

这种技术适用于任何背景,包括渐变和图像背景,我们稍后会讨论这些主题。但请记住:如果由于某种原因背景未能绘制在文本后面,那么本应“填充”背景的透明文本将完全不可读。

警告

截至 2022 年底,并非所有浏览器都正确支持background-clip: text。Blink 浏览器(Chrome 和 Edge)需要使用-webkit-前缀,支持-webkit-background-clip: text。此外,由于浏览器未来可能不再支持text值(在我们撰写本文时正在讨论从 CSS 中删除),请包括带有前缀和非前缀版本的background-clip,并在@supports功能查询中设置透明色(有关更多信息,请参见第二十一章)。

处理背景图像

在介绍了背景颜色的基础知识之后,我们现在转向背景图像的主题。默认情况下,图像是平铺的,在水平和垂直方向上重复,以填充整个文档的背景。这种默认的 CSS 行为曾经创造出一些可怕的网站,通常被称为“Geocities 1996”,但是 CSS 可以做的远不止简单的背景图像平铺。它可以用来创造微妙的美感。我们将从基础开始,然后逐步深入。

使用图像

要首先将图像放置在背景中,请使用属性background-image

默认值为none意味着正如您所期望的那样:没有图像放置在背景中。如果您想要背景图像,您必须至少提供这个属性的一个图像引用,例如以下内容:

body {background-image: url(bg23.gif);}

由于其他背景属性的默认值,这将导致图像bg23.gif在文档的背景中平铺,如图 8-7 所示。稍后您将学习如何更改这一点。

css5 0807

图 8-7。在 CSS 中应用背景图像

最好指定一个背景颜色来配合背景图像;稍后我们会回到这个概念。(我们也会讨论如何同时使用多个图像,但现在我们只会使用一个背景图像。)

你可以将背景图像应用于任何元素,块级或行内。如果有多个背景图像,用逗号分隔它们:

body {background-image: url(bg23.gif), url(another_img.png);}

如果你将简单的图标与创意属性选择器结合起来,可以(通过我们即将介绍的属性)标记链接指向 PDF、文字处理器文档、电子邮件地址或其他不寻常的资源。例如,你可以使用以下代码显示图 8-8:

a[href] {padding-left: 1em; background-repeat: no-repeat;}
a[href$=".pdf"] {background-image: url(/i/pdf-icon.png);}
a[href$=".doc"] {background-image: url(/i/msword-icon.png);}
a[href^="mailto:"] {background-image: url(/i/email-icon.png);}

css5 0808

图 8-8. 将链接图标添加为背景图像

的确,你可以为一个元素添加多个背景图像,但在学会如何定位每个图像并防止其重复之前,你可能不会这样做。我们将在介绍这些必要属性后讨论重复背景图像。

就像background-color一样,background-image也不会被继承——事实上,背景属性中没有一个是继承的。同样要记住,当指定背景图像的 URL 时,它受到url()值通常的限制和注意事项:相对 URL 应该相对于样式表进行解释(参见“URLs”)。

理解为什么背景不会被继承

早些时候,我们特别指出背景不会被继承。背景图像展示了为什么继承背景将是一个坏主意。想象一下如果背景是继承的情况,你将一个背景图像应用于<body>,那么该图像将用作文档中每个元素的背景,每个元素都会执行自己的平铺,如图 8-9 所示。

css5 0809

图 8-9. 继承的背景会对布局产生什么影响

注意,图案在每个元素的左上角重新开始,包括链接。这并不是大多数作者想要的效果,这也是为什么背景属性不被继承的原因。如果出于某种原因你确实想要这种特定效果,你可以通过以下规则实现:

* {background-image: url(yinyang.png);}

或者,你可以像这样使用值inherit

body {background-image: url(yinyang.png);}
* {background-image: inherit;}

遵循良好的背景实践

图像叠放在你指定的背景颜色之上。如果你的图像没有平铺或者有不透明的区域,背景颜色会透过显示,将其颜色与半透明图像混合。如果图像加载失败,将显示指定的背景颜色而不是图像。因此,当使用背景图像时,最好指定一个背景颜色,这样即使图像不出现,你也能得到一个可读的结果。

背景图像可能会导致可访问性问题。例如,如果您将晴朗蓝天的图像作为背景,上面放置深色文本,那么通常很易读。但如果天空中有一只鸟呢?如果深色文本落在背景的深色部分上,则文本将不可读。为文本添加阴影(请参见第十五章)或在所有文本后面添加半透明背景色可以减少非可读风险。

定位背景图像

好的,我们可以将图像放置在元素的背景中。想要精确定位图像怎么样?没问题!background-position 属性来帮忙。

那些值的语法看起来可怕,但实际上并非如此;这只是当您试图将新技术的快速实现形式化为常规语法,并在此基础上叠加更多功能时所产生的情况。实际上,background-position 的语法很简单,但百分比值可能有点难以理解。

注意

在本节中,我们将使用规则 background-repeat: no-repeat 来阻止背景图像的平铺。您没有想错:我们还没有讨论 background-repeat!目前,只需接受这条规则限制了背景为单一图像。您将在 “背景重复(或缺少)” 中了解更多细节。

例如,我们可以在 <body> 元素中居中背景图像,如下所示,并在 图 8-10 中显示结果:

body {background-image: url(hazard-rad.png);
    background-repeat: no-repeat;
    background-position: center;}

css5 0810

图 8-10. 居中单个背景图片

在这里,我们将一个单一图像放在背景中,并使用 background-repeat 阻止其重复。每个包含图像的背景都以一个单一图像开始。这个起始图像称为原始图像

使用 background-position 完成原始图像的定位,有几种方法可以为此属性提供值。首先,我们可以使用关键词 topbottomleftrightcenter。通常,这些关键词会成对出现,但(正如前面的例子所示)并非总是如此。我们还可以使用长度值,如 50px2cm;关键词和长度值的组合,如 right 50px bottom 2cm;最后,百分比值,如 43%。每种类型的值对背景图像的放置有稍微不同的影响。

关键词

图像放置关键词最容易理解。它们具有预期的效果;例如,top right会使原始图像位于元素背景的右上角。让我们使用一个小的阴阳符号:

p {background-image: url(yinyang-sm.png);
    background-repeat: no-repeat;
    background-position: top right;}

这将在每个段落背景的右上角放置一个非重复的原始图像,如果位置声明为right top,结果将完全相同。

这是因为位置关键词可以以任何顺序出现,只要不超过两个——一个用于水平和一个用于垂直。如果使用两个水平(right right)或两个垂直(top top)关键词,整个值都将被忽略。

如果只有一个关键词出现,则另一个假定为center。因此,如果您希望图像出现在每个段落顶部中心位置,只需声明如下:

p {background-image: url(yinyang-sm.png);
    background-repeat: no-repeat;
    background-position: top;} /* same as 'top center' */

百分比值

百分比值与关键词紧密相关,尽管它们的行为方式更为复杂。假设您想要使用百分比值将原始图像在其元素内居中。这非常简单:

p {background-image: url(chrome.jpg);
   background-repeat: no-repeat;
   background-position: 50% 50%;}

这会使原始图像的中心与其元素背景的中心对齐。换句话说,百分比值适用于元素和原始图像。在图像中从顶部和左侧各 50%处的像素将放置在其设置的元素的顶部和左侧各 50%处。

要理解这意味着什么,让我们更详细地检查这个过程。当您在元素的背景中心放置原始图像时,可以描述为50% 50%(中心)的图像点与可以以同样方式描述的背景点对齐。如果将图像放置在0% 0%,其左上角将放置在元素背景的左上角。使用100% 100%会导致原始图像的右下角进入背景的右下角。图 8-11 包含了这些值的示例,以及几个其他值,每个值的对齐点位于同心圆的中心。

因此,如果您想要将单个原始图像放置在背景的横向三分之一和纵向两分之一处,则声明如下:

p {background-image: url(yinyang-sm.png);
   background-repeat: no-repeat;
   background-position: 33% 66%;}

根据这些规则,图像中从左上角横向三分之一,纵向向下两分之一处的点将与距离左上角背景最远的点对齐。请注意,水平值在百分比值中始终排在前面。如果您在前面的示例中交换百分比,图像中距离左侧两分之一和顶部向下三分之一处的点将被放置在背景横向两分之一和垂直向下三分之一处。

css5 0811

图 8-11. 不同的百分比位置

如果您只提供一个百分比值,则假定提供的是水平值,垂直值假定为50%。例如:

p {background-image: url(yinyang-sm.png);
    background-repeat: no-repeat;
    background-position: 25%;}

原始图像位于段落背景的四分之一处,并且在中间位置,就好像设置了background-position: 25% 50%;

表 8-1 提供了关键字和百分比等效的详细信息。

表 8-1. 位置等效

关键字 等效关键字 等效百分比
中间 中间 50% 50% 50%
中右 右中 100% 50% 100%
中左 左中 0% 50% 0%
顶部 顶部中间 中间顶部 50% 0%
底部 底部中间 中间底部 50% 100%
左上 左上 0% 0%
右上 右上 100% 0%
右下 右下 100% 100%
左下 左下 0% 100%

如“背景图像定位”中的属性表所示,background-position的默认值为0% 0%,在功能上与左上相同。这就是为什么除非您为位置设置不同的值,否则背景图像始终从元素背景的左上角开始平铺的原因。

长度值

最后,我们来讨论位置定位的长度值。当您为原始图像的位置提供长度时,它们被解释为从元素背景的左上角偏移的距离。偏移点是原始图像的左上角;因此,如果您设置值20px 30px,则原始图像的左上角将位于元素背景的左上角的右侧 20 像素,并且下方 30 像素,正如图 8-12 中所示的几个长度示例。与百分比一样,水平值在长度值中始续放在第一位。

css5 0812

图 8-12. 通过使用长度度量来偏移背景图像

这与百分比值有很大不同,因为偏移量是从一个左上角到另一个左上角。换句话说,原始图像的左上角与background-position声明中指定的点对齐。

您可以组合长度和百分比值以获得“两全其美”的效果。比如说,您需要一个背景图像,它位于背景的右侧并且距离顶部下降 10 像素。如常,水平值首先出现:

p {background-image: url(yinyang.png);
    background-repeat: no-repeat;
    background-position: 100% 10px;
    border: 1px dotted gray;}

事实上,通过使用right 10px也可以得到相同的结果,因为可以将关键字与长度和百分比混合使用。当使用非关键字值时,语法要求轴的顺序;如果使用长度或百分比值,则水平值必须始终放在第一位,垂直值必须始终放在第二位。这意味着right 10px是合法的,而10px right是无效的且将被忽略(因为right不是有效的垂直关键字)。

负值

如果您使用长度或百分比,可以使用负值将原始图像拉出元素的背景。考虑一个具有非常大的阴阳符号背景的文档。如果我们只希望在元素背景的左上角看到其中的一部分,理论上没有问题。

假设原始图像高 300 像素,宽 300 像素,并且只有图像的右下角三分之一应该可见,则我们得到所需效果(如 图 8-13 所示)如下:

body {background-image: url(yinyang.png);
    background-repeat: no-repeat;
    background-position: -200px -200px;}

css5 0813

图 8-13. 使用负长度值来定位原始图像

或者说,您希望仅在原始图像的右半部分可见,并且在元素的背景区域内垂直居中:

body {background-image: url(yinyang.png);
    background-repeat: no-repeat;
    background-position: -150px 50%;}

负值稍后将发挥作用,因为它们在创建华丽背景时非常有用;参见 “锥形渐变”。

负百分比也是可能的,尽管计算起来有些有趣。例如,原始图像和元素可能大小差异很大,这可能导致意想不到的效果。例如,考虑以下规则造成的情况,详见 图 8-14:

p {background-image: url(pix/yinyang.png);
    background-repeat: no-repeat;
    background-position: -10% -10%;
    width: 500px;}

css5 0814

图 8-14. 负百分比值的不同效果

规则要求由 -10% -10% 定义的原始图像外的点与每个段落的类似点对齐。图像尺寸为 300 × 300 像素,因此我们知道其对齐点可以描述为图像顶部上方 30 像素,左边缘左侧 30 像素(实际上是 -30px-30px)。段落元素的宽度均为 500px,因此水平对齐点在它们的背景左边缘左侧 50 像素处。这意味着每个原始图像的左边缘将位于段落左内边距边缘左侧 20 像素处。这是因为图像的 -30px 对齐点与段落的 -50px 点对齐。两者之间的差异为 20 像素。

但是,段落的高度各不相同,因此每个段落的垂直对齐点也会随之改变。如果段落的背景区域高度为 300 像素,随机选取一个例子,那么原始图像的顶部将与元素背景的顶部完全对齐,因为两者的垂直对齐点都是 -30px。如果段落高度为 50 像素,则其对齐点将是 -5px,原始图像的顶部实际上将比背景的顶部低 25 像素。这就是为什么您可以在 图 8-14 中看到所有背景图像的顶部——段落比背景图像短。

更改偏移边缘

是时候坦白了:在整个背景定位的讨论中,我们对你隐瞒了两个事实。我们表现得好像background-position的值最多只能有两个关键字,并且所有的偏移都总是从背景区域的左上角开始。

最初是这样的,但现在不再是这样了。当我们以非常特定的模式包括四个关键字,或者两个关键字和两个长度或百分比值时,可以设置背景图像应偏移的边缘。

让我们从一个简单的例子开始:将原始图像放置在距离左上角四分之一处并向下偏移 30 像素。根据前面章节的内容,那将是以下内容:

background-position: 25% 30px;

现在让我们用这个四部分语法做同样的事情:

background-position: left 25% top 30px;

这个四部分的值表示,“从边缘开始,水平偏移25%;从顶部边缘开始,偏移30px。”

很好,这是一种更详细的方法来获得默认行为。现在让我们改变代码,使得原始图像放置在距离右下角四分之一处并距离底部 30 像素的位置,如图 8-15 所示(假设背景图像不重复,以便清楚明了):

background-position: right 25% bottom 30px;

在这里,我们有一个值,意思是“从边缘开始,水平偏移25%;从底部边缘开始,偏移30px。”

因此,一般的模式是边缘关键字、偏移距离、边缘关键字、偏移距离。你可以混合水平和垂直信息的顺序;例如,bottom 30px right 25%right 25% bottom 30px都可以正常工作。然而,你不能省略任何一个边缘关键字;30px right 25%是无效的,并且会被忽略。

css5 0815

图 8-15. 改变原始图像的偏移边缘

也就是说,当你希望偏移距离为 0 时,可以省略偏移距离。因此,right bottom 30px会将原始图像放在背景区域右边缘并距离底部 30 像素的位置,而right 25% bottom会将原始图像放在距离右边缘四分之一处并靠近底部的位置。这两者都在图 8-16 中有图示。

css5 0816

图 8-16. 推断出的零长度偏移

你只能将元素的边缘定义为偏移基准,而不能定义中心。像center 25% center 25px这样的值会被忽略。

如果你有多个背景图像,但只有一个背景位置,那么所有图像将放置在同一个位置。如果你希望它们放置在不同的位置,请提供一个以逗号分隔的背景位置列表。它们将按顺序应用于图像。如果图像比位置值多,位置将重复出现(我们将在本章后面进一步探讨)。

改变定位框

现在您知道如何将图像添加到背景中,甚至可以更改原点图像的放置位置。但是,如果我们希望将其放置在边框边缘或外部内容边缘,而不是默认的外部填充边缘,我们可以使用属性background-origin

此属性可能看起来类似于background-clip,这是有道理的,但其效果是不同的。background-clip定义了背景绘制区域,而background-origin定义了用于确定原点图像放置位置的边缘。这也称为定义背景定位区域

默认情况下,padding-box意味着原点图像的左上角将被放置在元素的填充框外边缘的左上角(如果background-position未从其默认值top left0 0更改),即在边框区域内部。

如果使用值border-box,则background-position: 0 0的原点图像的左上角将进入填充区域的左上角。如果存在边框,则会在原点图像之上绘制(假设背景绘制区域未限制为padding-boxcontent-box)。

使用content-box,您将原点图像移至内容区域的左上角。以下代码描绘了在图 8-17 中所示的三个选项。

div[id] {color: navy; background: silver;
         background-image: url(yinyang.png);
         background-repeat: no-repeat;
         padding: 1em; border: 0.5em dashed;}
#ex01 {background-origin: border-box;}
#ex02 {background-origin: padding-box;}  /* default value */
#ex03 {background-origin: content-box;}

css5 0817

图 8-17. 背景来源的三种类型

请记住,“放置在左上角”的行为是默认行为,您可以通过background-position进行更改。原点图像的位置是相对于由background-origin定义的框(边框边缘、填充边缘或内容边缘)来计算的。例如,考虑我们先前示例的这个变体,如图 8-18 所示:

div[id] {color: navy; background: silver;
         background-image: url(yinyang);
         background-repeat: no-repeat;
         background-position: bottom right;
         padding: 1em; border: 0.5em dashed;}
#ex01 {background-origin: border-box;}
#ex02 {background-origin: padding-box;}  /* default value */
#ex03 {background-origin: content-box;}

css5 0818

图 8-18. 背景来源的三种类型,重制版

如果您明确定义了背景原点和裁剪为不同的框,则情况会变得非常有趣。想象一下,您已将原点放置在填充边缘,但背景被裁剪到内容区域,或者反之。以下代码结果显示在图 8-19 中:

 #ex01 {background-origin: padding-box;
        background-clip: content-box;}
 #ex02 {background-origin: content-box;
        background-clip: padding-box;}

css5 0819

图 8-19. 原点和裁剪不一致时

在第一个示例中,由于原点图像是相对于填充框定位的,所以原点图像的边缘被裁剪,但是背景绘制区域已在内容框的边缘处被裁剪。在第二个示例中,原点图像是相对于内容框放置的,但是绘制区域延伸到了填充框内部。因此,原点图像可见一直延伸到底部填充边缘,即使其顶部未放置在顶部填充边缘。

背景重复(或其缺乏)

视口有无限多种尺寸。幸运的是,我们可以平铺背景图像,这意味着我们不需要创建多种尺寸的背景,也不必为小屏幕低带宽设备提供大尺寸(和文件大小)的壁纸。当您想以特定方式重复图像或根本不想重复时,可以使用 background-repeat

background-repeat 的值语法乍看起来有点复杂,但实际上非常简单。事实上,它基本上只使用了四个值:repeatno-repeatspaceround。其余两个值 repeat-xrepeat-y 被视为其他值的组合方式。Table 8-2 显示了它们的具体含义。

如果给定了两个值,则第一个值适用于水平方向,第二个值适用于垂直方向。如果只有一个值,则适用于水平和垂直方向,除了如 Table 8-2 所示的 repeat-xrepeat-y 例外。

Table 8-2. 重复关键字的等效表示

单关键字 等效关键字
repeat-x repeat no-repeat
repeat-y no-repeat repeat
repeat repeat repeat
no-repeat no-repeat no-repeat
space space space
round round round

正如您可能猜到的那样,repeat 单独会导致图像在所有方向上无限平铺。repeat-xrepeat-y 值分别导致图像在水平或垂直方向重复,并且 no-repeat 防止图像沿指定轴线重复。如果有多个图像,每个图像的重复模式不同,请提供一个逗号分隔的值列表。我们说“所有方向”而不是“两个方向”,因为 background-position 可能会将初始重复图像放在裁剪框的左上角之外。使用 repeat,图像在所有方向上重复。默认情况下,背景图像将从元素的左上角开始。因此,以下规则将产生 Figure 8-20 中显示的效果:

body {background-image: url(yinyang-sm.png);
      background-repeat: repeat-y;}

css5 0820

Figure 8-20. 垂直平铺背景图像

假设您只想让图像在文档顶部重复。与其创建一个下面有很多空白空间的特殊图像,不如只需对最后一个规则进行小改动:

body {background-image: url(yinyang-sm.png);
      background-repeat: repeat-x;}

如 Figure 8-21 所示,图像在 x 轴(水平方向)从其起始位置开始重复——在这种情况下,是 <body> 元素背景区域的左上角。

css5 0821

Figure 8-21. 水平平铺背景图像

最后,您可能不想重复背景图像。在这种情况下,请使用值 no-repeat

body {background-image: url(yinyang-sm.png);
      background-repeat: no-repeat;}

对于这个小图像来说,no-repeat可能看起来并不是非常有用,但它是最常见的值,不幸的是不是默认值。让我们再试一次,使用一个更大的符号。以下代码的结果是图 8-22:

body {background-image: url(yinyang.png);
      background-repeat: no-repeat;}

css5 0822

图 8-22. 放置单个大背景图像

控制重复方向的能力极大地扩展了可能的效果范围。例如,假设你想要在文档中每个<h1>元素的左侧设置三重边框。你可以进一步将这个概念推广,并决定在每个<h2>元素的顶部设置波浪边框。这张图片的着色与背景色混合,产生了图 8-23 所示的波浪效果,这是以下代码的结果:

h1 {background-image: url(triplebor.gif); background-repeat: repeat-y;}
h2 {background-image: url(wavybord.gif); background-repeat: repeat-x;
    background-color: #CCC;}

css5 0823

图 8-23. 使用背景图像边框元素
提示

创建波浪边框效果有更好的方法——特别是在“图像边框”中探讨的边框图像属性。

定位重复的图像

在前一节中,我们探讨了repeat-xrepeat-yrepeat的值,以及它们如何影响背景图像的平铺。在每种情况下,平铺图案始终从元素背景的左上角开始。这是因为,正如你所看到的,background-position的默认值为0% 0%。鉴于你已经知道如何改变原始图像的位置,你需要了解用户代理如何处理它。

更容易通过示例展示然后解释它。考虑以下标记,它在图 8-24 中有所说明:

p {background-image: url(yinyang-sm.png);
    background-position: center;
    border: 1px dotted gray;}
p.c1 {background-repeat: repeat-y;}
p.c2 {background-repeat: repeat-x;}

css5 0824

图 8-24. 将原始图像居中并重复显示

所以你看到了:条纹贯穿元素中心。它看起来可能是错误的,但实际上并不是。

这些例子是正确的,因为原始图像已经放置在第一个<p>元素的中心。在第一个例子中,图像沿着 y 轴在上下两个方向上铺设,从中心的原始图像开始。在第二个例子中,图像沿 x 轴铺设,从原始图像开始,并重复到右侧和左侧。你可能注意到第一个和最后一个重复略有被截断,而当我们从background-position: 0 0开始时,只有最后一个图像或最右边和最下边的图像可能被剪切。

<p>中设置一张图像并让其完全重复会导致其在四个方向上平铺:上、下、左、右。 background-position唯一影响的是平铺开始的位置。当背景图像从中心重复时,阴阳符号的网格会居中在元素内部,导致沿边缘的一致裁剪。当平铺从填充区域的左上角开始时,边缘周围的裁剪不一致。另一方面,spacingrounding值可以防止图像裁剪,但它们也有自己的缺点。

注意

如果你好奇的话,CSS 没有像repeat-leftrepeat-up这样的单方向值。

间隔和舍入重复模式

除了到目前为止看到的基本平铺模式外,background-repeat还能够精确填充背景区域。例如,考虑如果使用值space定义平铺模式会发生什么,如图 8-25 所示:

div#example {background-image: url(yinyang.png);
            background-repeat: space;}

css5 0825

图 8-25. 使用填充空间平铺背景图像

您会注意到元素的四个角落中都有背景图像。此外,这些图像间隔开来,使它们在水平和垂直方向上都以规则间隔发生。

space的作用是:它确定沿着给定轴线完全适合的重复次数,然后以规则间隔将它们间隔开,使重复从背景的一边到另一边。这并不保证一个规则的正方形网格,水平和垂直方向上的间隔都是相同的。它只是意味着你将拥有看起来像是背景图像的列和行。虽然不会裁剪任何图像,除非连一个迭代都没有足够的空间(这在非常大的背景图像上可能发生),但这个值通常会导致水平和垂直分隔不同。图 8-26 展示了一些例子。

css5 0826

图 8-26. 使用不同间隔平铺显示background-repeat: space在不同大小元素上的效果
注意

请记住,任何背景颜色或元素的“背景”(即元素祖先的组合背景)都会显示在通过space分隔的背景图像之间的空隙中。

如果有一张很大的图像,在给定的轴线上不能重复超过一次,甚至一次,会发生什么情况?该图像将根据background-position的值绘制一次,并根据需要进行裁剪。反之,如果在某个轴线上可以容纳多次图像重复,那么background-position的值在该轴线上将被忽略。例如,以下代码显示图 8-27:

div#example {background-image: url(yinyang.png);
            background-position: center;
            background-repeat: space;}

css5 0827

图 8-27. 沿一个轴线间隔但不沿另一个轴线间隔

注意图像在水平方向上的间距,因此覆盖了沿该轴的 center 位置,但在垂直方向上居中且没有间距(因为没有足够的空间来这样做)。这是 space 在一个轴上覆盖 center 而在另一个轴上不覆盖的效果。

相比之下,值 round 可能会导致背景图像在重复时进行缩放,并且 (奇怪的是)不会覆盖 background-position。如果一张图像不能完全重复以适应背景的边缘,那么这张图像将被放大 或者 缩小以使其适合整数倍。

此外,图像可以沿每个轴向不同比例缩放。 round 值是唯一可以根据需要自动改变图像固有纵横比的背景属性值。虽然 background-size 也可以导致纵横比发生变化,从而扭曲图像,但这只有在作者明确指示时才会发生。你可以在 图 8-28 中看到一个示例,这是以下代码的结果:

body {background-image: url(yinyang.png);
      background-position: top left;
      background-repeat: round;}

css5 0828

图 8-28. 带有缩放的背景图像平铺

注意,如果你有一个宽度为 850 像素的背景和一个水平圆角图像宽度为 300 像素,浏览器可以决定使用三张图像,并将它们缩小以适应 850 像素区域中的三张图像(因此每个图像实例宽度为 283.333 像素)。使用 space,浏览器将使用两张图像并在它们之间放置 250 像素的空间,但 round 没有这种限制。

这里有一个有趣的变化:虽然 round 会调整图像大小,使其能够整数倍地适应背景,但它 不会 移动图像以确保它们实际触及背景的边缘。确保重复图案适合且没有背景图像被裁剪的唯一方法是将原始图像放在一个角落。如果原始图像在其他地方,将会发生裁剪。以下代码展示了一个例子,详见 图 8-29:

body {background-image: url(yinyang.png);
      background-position: center;
      background-repeat: round;}

css5 0829

图 8-29. 被剪切的圆角背景图像

图像仍然按比例缩放,以便它们能够整数倍地适应背景定位区域。它们只是没有重新定位以实际做到这一点。因此,如果你打算使用 round,并且不想有任何被裁剪的背景图块,请确保你从四个角落之一开始(并确保背景定位和绘制区域相同;更多信息请参见 “平铺和剪切重复的背景”)。

平铺和剪切重复的背景

正如你可能记得的,background-clip 可以改变背景绘制的区域,而 background-origin 决定了原始图像的放置位置。那么当你将剪切区域和原始区域设置为不同,并且 使用 spaceround 作为平铺模式时会发生什么呢?

基本答案是,如果background-originbackground-clip的值不同,就会发生剪切。这是因为spaceround是相对于背景定位区域而不是绘制区域计算的。图 8-30 显示了可能发生的一些示例。

css5 0830

图 8-30. 由于剪切和原点值不匹配而导致的剪切

至于使用的最佳值组合,这是一个见仁见智的问题。在大多数情况下,将background-originbackground-clip都设置为padding-box可能会得到你想要的结果。但如果你打算有透明部分的边框,那么border-box可能是一个更好的选择。

获得附件

现在你知道如何在元素背景的任何位置放置原始图像,也知道如何(在很大程度上)控制其平铺方式。你可能已经意识到,将图像放在<body>元素的中心可能意味着,如果文档足够长,背景图像对于读者来说可能一开始是不可见的。毕竟,浏览器是一个视口,提供对文档的窗口。如果文档太长而无法完全显示在视口中,用户可以通过文档来回滚动。身体的中心可能比文档的开始低两到三个“屏幕”,或者足够远以将大部分原始图像推到浏览器窗口底部之外。

此外,如果初始时可见原始图像,默认情况下,它会随文档一起滚动——当用户滚动超出图像位置时消失。不用担心:CSS 提供了一种方法防止背景图像滚出视野。

使用属性background-attachment,可以声明原始图像固定在视区,因此不受滚动影响:

body {background-image: url(yinyang.png);
    background-repeat: no-repeat;
    background-position: center;
    background-attachment: fixed;}

这样做有两个直接的影响。首先,原始图像不会随文档一起滚动。其次,原始图像的放置由视口的大小决定,而不是包含它的元素的大小(或在视口内的放置)。图 8-31 显示即使文档部分滚动到文本中间,图像仍然位于视口中心。

css5 0831

图 8-31. 中心对齐继续保持

fixed的元素特定版本是local。然而,在这种情况下,效果仅在需要滚动元素内容(而不是整个文档)时可见。这一点起初可能有些难以理解。考虑以下情况,其中background-attachment默认为scroll

aside {background-image: url(yinyang.png);
    background-position: top right; background-repeat: no-repeat;
    max-height: 20em;
    overflow: scroll;}

在这种情况下,如果aside的内容高度超过 20 em,则溢出的内容不可见,但可以通过滚动条访问。然而,背景图像不会随内容滚动,而是会停留在元素框的右上角。

通过添加background-attachment: local,图像附加到本地上下文。如果您有使用iframe的经验,视觉效果就像iframe。图 8-32 显示了先前代码示例和下面的代码并排的结果:

aside {background-image: url(yinyang.png);
    background-position: top right; background-repeat: no-repeat;
    background-attachment: local; /* attaches to content */
    max-height: 20em;
    overflow: scroll;}

css5 0832

图 8-32. 默认滚动附件与本地附件的比较

background-attachment的另一个值是默认值scroll。正如您所料,这会导致在 Web 浏览器中查看文档时,背景图像随着文档的其余部分滚动,并且在调整窗口大小时不一定会更改原始图像的位置。如果文档宽度固定(例如通过为<body>元素分配显式width),调整视图区域的大小不会影响滚动附加原始图像的放置。

附加背景的有用副作用

在技术术语中,当背景图像被固定时,它相对于视口进行定位,而不是包含它的元素。然而,背景只会在其包含的元素内可见。将图像与视口对齐,而不是元素,可以利用我们的优势。

假设您有一个文档,具有实际上看起来像平铺的背景,并且<h1><h2>元素都具有相同的模式,只是颜色不同。您可以将<body>和标题元素都设置为具有固定背景,如下所示,导致图 8-33:

body {background-image: url(grid1.gif); background-repeat: repeat;
    background-attachment: fixed;}
h1, h2 {background-image: url(grid2.gif); background-repeat: repeat;
    background-attachment: fixed;}

这个巧妙的技巧是因为当背景附件为fixed时,原始元素相对于视口定位。因此,两个背景模式从视口的左上角开始平铺,而不是从各个元素开始。对于<body>,您可以看到整个重复图案。然而,对于<h1>,您唯一可以看到其背景的地方是在<h1>本身的填充和内容中。由于两个背景图像大小相同并且具有完全相同的起源,它们看起来是对齐的,如图 8-33 所示。

css5 0833

图 8-33. 背景完美对齐

这种能力可以用来创建复杂的效果。其中一个最著名的例子是“复杂螺旋变形”演示,如图 8-34 所示。

css5 0834

图 8-34. 复杂螺旋变形

这些视觉效果是通过将不是<body>元素的不同固定附加背景图像分配给它们来实现的。整个演示由一个 HTML 文档、四张 JPEG 图像和一个样式表驱动。因为所有四张图像都位于浏览器窗口的左上角,但只有在与其元素交集的地方可见,这些图像排列起来创建了半透明波纹玻璃的幻觉。(现在我们可以使用 SVG 滤镜来实现这些特效,但是固定附加背景曾在 2002 年创造了虚假的滤镜效果。)

还有一个情况,在页面媒体中(如打印品)每页都会生成自己的视口。因此,固定附加背景应出现在打印品的每一页上。这可以用于诸如在文档的所有页面上打水印等效果。

设置背景图像大小

到目前为止,我们已经获取了各种大小的图像,并将它们放入元素背景中进行重复(或不重复)、定位、裁剪和附加。在每种情况下,我们只是采用了图像的固有尺寸(除了自动例外的round重复)。准备好实际更改原始图像的大小以及由此产生的所有平铺图像吗?

让我们从明确调整背景图像开始。我们将插入一张 200 × 200 像素的图像,然后将其调整为两倍大小。以下代码将得到图 8-35 的结果:

main {background-image: url(yinyang.png);
    background-repeat: no-repeat;
    background-position: center;
    background-size: 400px 400px;}

css5 0835

图 8-35. 调整原始图像大小

使用background-size,我们可以将原始图像调整为更小的尺寸。我们可以使用 ems、像素、视口宽度、任何长度单位或它们的组合来设置其大小。

我们甚至可以通过改变其大小来扭曲图像。当将上述代码示例更改为使用background-size: 400px 4em时,图 8-36 展示了其结果,包括重复和非重复背景。

css5 0836

图 8-36. 通过调整大小来扭曲原始图像

如您所见,当background-size有两个值时,第一个是水平大小,第二个是垂直大小。如果允许图像重复,所有重复的图像将与原始图像相同大小。

百分比更有趣。如果声明百分比值,则其计算是相对于背景定位区域来进行的,即由background-origin而不是background-clip定义的区域。假设您希望图像的宽度和高度为其背景定位区域的一半,则以下代码将得到图 8-37 的结果:

background-size: 50% 50%;

css5 0837

图 8-37. 使用百分比调整原始图像大小

是的,您可以混合长度和百分比:

background-size: 25px 100%;

不允许为background-size使用负长度和百分比值。

保持背景图像的宽高比

现在,对于 auto 的默认值是什么?首先,当只提供一个值时,它被视为水平尺寸,而垂直尺寸则设置为 auto。(因此 background-size: auto 等同于 background-size: auto auto。)如果你想要垂直尺寸自适应而保留图像的固有宽高比,你必须明确地写出来,像这样:

background-size: auto 333px;

在许多方面,background-size 中的 auto 表现得像是应用于替换元素(例如图像)的 heightwidth(也是 block-sizeinline-size)的 auto 值。也就是说,如果它们被应用于不同上下文中相同的图像,你会期望从以下两个规则中得到大致相似的结果:

img.yinyang {width: 300px; height: auto;}

main {background-image: url(yinyang.png);
    background-repeat: no-repeat;
    background-size: 300px auto;}

覆盖和包含

现在让我们来玩得更开心点!假设你想要用一张图像覆盖元素的整个背景,并且不在乎部分图像超出背景绘制区域。在这种情况下,你可以使用 cover

main {background-image: url(yinyang.png);
    background-position: center;
    background-size: cover;}

这会缩放原始图像,使其完全覆盖背景定位区域,同时仍保持其固有宽高比,假设它有一个。你可以在 图 8-38 中看到一个例子,其中一个 200 × 200 像素的图像被放大以覆盖一个 800 × 400 像素元素的背景。以下代码提供了这个结果:

main {width: 800px; height: 400px;
    background-image: url(yinyang.png);
    background-position: center;
    background-size: cover;}

请注意,这个示例中没有 background-repeat。这是因为我们期望图像填满整个背景,所以是否重复并不重要。

你还可以看到 cover100% 100% 是非常不同的。如果我们使用了 100% 100%,原始图像将会被拉伸至 800 像素宽 × 400 像素高。而 cover 让它变成了 800 像素宽和高,并将图像居中显示在背景定位区域内。在这种特定情况下,它与 100% auto 的效果相同,但 cover 的美妙之处在于它无论元素是宽还是高都能工作。

css5 0838

图 8-38. 用原始图像覆盖背景

相比之下,contain 将会缩放图像,使其恰好适应背景定位区域内部,即使这样会留下一些背景显示在它周围。这在 图 8-39 中有所展示,这是以下代码的结果:

main {width: 800px; height: 400px;
    background-image: url(yinyang.png);
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;}

css5 0839

图 8-39. 包含原始图像在背景内

在这种情况下,由于元素的高度小于宽度,原始图像被缩放至与背景定位区域一样高,并且宽度也被缩放以匹配,就像我们声明了 auto 100% 一样。如果一个元素比它宽,则 contain 的行为类似于 100% auto

请注意,我们在示例中重新引入了 no-repeat,以防视觉效果过于混乱。如果删除该声明,背景将重复,如果这是您想要的话,也没什么大不了的。图 8-40 显示了结果。

css5 0840

图 8-40. 重复包含原点图像

始终记住:covercontain 图片的大小始终与背景定位区域有关,这由 background-origin 定义。即使由 background-clip 定义的背景绘制区域不同,这也是真实的!考虑下面的规则,这些规则在 图 8-41 中描述:

div {border: 1px solid red;
     background: url(yinyang-sm.png) center no-repeat green;}
     /* that’s shorthand 'background', explained in the next section */
.cover {background-size: cover;}
.contain {background-size: contain;}
.clip-content {background-clip: content-box;}
.clip-padding {background-clip: padding-box;}
.origin-content {background-origin: content-box;}
.origin-padding {background-origin: padding-box;}

css5 0841

图 8-41. 使用 background-clipbackground-origin 进行覆盖和包含

是的,您可以看到某些图像周围的背景色,而其他图像则被裁剪了。这是绘制区域和定位区域之间的区别。您可能认为 covercontain 的大小应该与绘制区域有关,但实际上并非如此,正如 图 8-41 中的最后几个示例所示。每当您使用这些值时,请牢记这一点。

如果您有多个背景图像,并具有不同的位置、重复或大小值,请包含一个逗号分隔的值列表。列表中的每个值将与列表中该位置的图像相关联。如果值比图像多,则额外的值将被忽略。如果值比图像少,则列表将重复。您只能设置一种背景颜色。

注意

在这一部分,我们使用了光栅图像(准确地说是 GIF),即使在放大时它们看起来很糟糕,在缩小时也会浪费网络资源。(我们这样做是为了在大量放大发生时更加明显。)这是在缩放背景光栅图像时的固有风险。另一方面,您可以轻松地使用 SVG 作为背景图像,它们在放大或缩小时不会失真或浪费带宽。如果您要缩放背景图像并且它不必是照片,强烈考虑使用 SVG 或 CSS 渐变。

全部综合起来

就像 CSS 的主题领域经常发生的情况一样,所有背景属性都可以合并为一个单独的简写属性:background。您是否想要这样做是另一个完全不同的问题。

这种语法可能有点令人困惑。让我们从简单的开始,逐步深入。

首先,以下语句都是相互等价的,并将产生 图 8-42 中所示的效果:

body {background-color: white;
      background-image: url(yinyang.png);
      background-position: top left;
      background-repeat: repeat-y;
      background-attachment: fixed;
      background-origin: padding-box;
      background-clip: border-box;
      background-size: 50% 50%;}
body {background:
    white url(yinyang.png) repeat-y top left/50% 50% fixed
     padding-box border-box;}
body {background:
    fixed url(yinyang.png) padding-box border-box white repeat-y
     top left/50% 50%;}
body {background:
    url(yinyang.png) top left/50% 50% padding-box white repeat-y
    fixed border-box;}

css5 0842

图 8-42. 使用简写

你可以根据自己的喜好大多混合排列值的顺序,但有三个限制。首先,任何background-size值必须紧跟在background-position值后面,并且用斜杠(/)分隔开。其次,在这些值内部,通常的限制仍然适用:水平值先,然后是垂直值,假设你提供的是轴派生的值(而不是例如cover)。

第三,如果你为background-originbackground-clip同时提供值,那么你列出的两个中的第一个将被分配给background-origin,第二个将被分配给background-clip。因此,以下两个规则在功能上是相同的:

body {background:
    url(yinyang.png) top left/50% 50% padding-box border-box white
     repeat-y fixed;}
body {background:
    url(yinyang.png) top left/50% 50% padding-box white repeat-y
     fixed border-box;}

与此相关,如果你只提供一个这样的值,它将同时设置background-originbackground-clip。因此,下面的简写将背景定位区域和背景绘画区域都设置为填充框:

body {background:
    url(yinyang.png) padding-box top left/50% 50% border-box;}

就像简写属性一样,如果你省略了任何值,相关属性的默认值将自动填充。因此,以下两个是等价的:

body {background: white url(yinyang.png;}
body {background: white url(yinyang.png) transparent 0% 0%/auto repeat
      scroll padding-box border-box;}

更好的是,background 没有必需的值——只要有至少一个值存在,就可以省略其余的。使用这个简写属性只设置背景颜色是一个非常常见的做法:

body {background: white;}

注意,background是一个简写属性,因此它的默认值可以覆盖给定元素的先前分配的值。例如:

h1, h2 {background: gray url(thetrees.jpg) center/contain repeat-x;}
h2 {background: silver;}

根据这些规则,<h1>元素将根据第一条规则进行样式设置。而<h2>元素将根据第二条进行样式设置,这意味着它们将只有一个平坦的银色背景。不会应用任何图像到<h2>的背景上,更别说水平居中并重复了。作者更可能是想要这样做:

h1, h2 {background: gray url(thetrees.jpg) center/contain repeat-x;}
h2 {background-color: silver;}

让背景颜色变化而不会清除所有其他值。

另一个限制将把我们很好地带入下一节:你只能为最后一个背景层提供背景颜色。没有其他背景层可以声明固体颜色。这是什么意思?很高兴你问。

使用多重背景

在本章的大部分内容中,我们只简要提到几乎所有背景属性都接受逗号分隔的值列表。例如,如果你想要三种不同的背景图像,可以这样做:

section {background-image: url(bg01.png), url(bg02.gif), url(bg03.jpg);
         background-repeat: no-repeat;}

真的。它会看起来像图 8-43。

css5 0843

图 8-43。多重背景图像

这样创建了三个背景层,每个图像一个层,最后一个是最终的底部背景层。

这三张图片叠放在元素的左上角,且不重复。不重复的原因是我们声明了background-repeat: no-repeat。我们只声明了一次,并且有三张背景图片。

当与背景相关的属性和background-image属性中的值数量不匹配时,缺少的值将通过在值不足的属性中重复序列来派生。因此,在前面的例子中,我们就像是这样说的:

background-repeat: no-repeat, no-repeat, no-repeat;

现在,假设我们想把第一张图像放在右上角,把第二张放在左侧中心,并把最后一层放在底部中心。我们可以如下层叠background-position,导致图 8-44:

section {background-image: url(bg01.png), url(bg02.gif), url(bg03.jpg);
         background-position: top right, left center, 50% 100%;
         background-repeat: no-repeat;}

css5 0844

图 8-44. 单独定位背景图像

同样地,假设我们想要保持前两层不重复,但水平重复第三层:

section {background-image: url(bg01.png), url(bg02.gif), url(bg03.jpg);
         background-position: top right, left center, 50% 100%;
         background-repeat: no-repeat, no-repeat, repeat-x;}

几乎每个背景属性都可以用逗号列表方式列出。您可以为每个创建的背景层设置不同的起源、裁剪框、大小以及几乎所有其他属性。从技术上讲,您可以拥有任意数量的背景层,尽管在某个时刻,这只会变得愚蠢起来。

即使是缩写的background也可以用逗号分隔。以下示例与前一个示例完全等效,结果显示在图 8-45 中:

section {
    background: url(bg01.png) right top no-repeat,
                url(bg02.gif) center left no-repeat,
                url(bg03.jpg) 50% 100% repeat-x;}

css5 0845

图 8-45. 通过缩写添加多个背景层

对于多个背景,唯一的真正限制是background-color不能像这样重复,并且如果您为background的缩写提供逗号分隔的列表,颜色只能出现在最后的背景层上。如果您将颜色添加到任何其他层上,整个background声明将无效。因此,如果我们想为前面的例子添加绿色背景填充,可以通过以下两种方式之一完成:

section {
    background: url(bg01.png) right top no-repeat,
                url(bg02.gif) center left no-repeat,
                url(bg03.jpg) 50% 100% repeat-x green;}
section {
    background: url(bg01.png) right top no-repeat,
                url(bg02.gif) center left no-repeat,
                url(bg03.jpg) 50% 100% repeat-x;
    background-color: green;}

这种限制的原因非常简单。想象一下,如果您能够将完整的背景颜色添加到第一层背景中。它将填充整个背景并遮挡所有背景层后面的内容!因此,如果您提供了颜色,它只能出现在最后一层,即“最底层”。

尽快内化这种排序是很重要的,因为它与您在使用 CSS 过程中建立的直觉恰恰相反。毕竟,您知道这里会发生什么——<h1>的背景将是绿色:

h1 {background-color: red;}
h1 {background-color: green;}

与此形成对比的是这个多背景规则,将使<h1>的背景变成红色:

h1 {background:
    url(box-red.gif),
    url(box-green.gif),
    green;}

是的,红色。红色的 GIF 图像平铺在整个背景区域,绿色的也是如此,但红色的 GIF 图像“覆盖”在绿色的上方。它离你更近。而效果恰好与内置级联“最后一个胜出”规则完全相反。

你可以这样想象:当存在多个背景时,它们像 Adobe Photoshop 或 Illustrator 等绘图程序中的图层一样列出。在绘图程序的图层面板中,顶部的图层会覆盖底部的图层。同样的过程也适用于这里:列表顶部的图层会覆盖列表底部的图层。

在某个时候,你很有可能会因为下意识按照级联顺序设置错误的一堆背景层次,因为你的反射弧会被激活。(即使到了今天,这个错误有时候也会让作者们感到困惑,所以如果你也遇到了,不要太沮丧。)

当你刚开始使用多个背景时,另一个相当常见的错误是使用background简写,并忘记通过让background-repeat的值默认为repeat来显式关闭背景层的平铺,从而遮蔽除顶部层以外的所有内容。例如,参见图 8-46,这是以下代码的结果:

section {background-image: url(bg02.gif), url(bg03.jpg);}

我们只能看到顶部层,因为它在无限平铺,这要归功于background-repeat的默认值。这就是为什么本节开头的例子使用了background-repeat: no-repeat

css5 0846

图 8-46. 用重复的图像遮蔽层

使用背景简写

避免这些情况的一种方法是使用background简写,如下所示:

body {background:
         url(bg01.png) top left border-box no-repeat,
         url(bg02.gif) bottom center padding-box no-repeat,
         url(bg04.svg) bottom center padding-box no-repeat gray;}

因此,当你添加或删除背景层时,你希望专门应用于它们的值也会随之而来或随之而去。

如果所有的背景都应该具有同一个给定属性的值(如background-origin),这可能意味着一些恼人的重复。如果情况如此,你可以混合这两种方法,如下所示:

body {background:
         url(bg01.png) top left no-repeat,
         url(bg02.gif) bottom center no-repeat,
         url(bg04.svg) bottom center no-repeat gray;
     background-origin: padding-box;}

只要你不需要做任何异常情况,这种方法就可以工作。一旦你决定改变其中一个背景层的起源,你将需要显式列出它们,无论是在background中还是使用单独的background-origin声明。

记住,层的数量由背景图像的数量决定,因此,根据定义,background-image的值并不重复以等于其他属性的逗号分隔值的数量。你可能想把同一张图像放在元素的四个角落,并认为你可以这样做:

background-image: url(i/box-red.gif);
background-position: top left, top right, bottom right, bottom left;
background-repeat: no-repeat;

然而,结果将是将一个单独的红色框放置在元素的左上角。要像图 8-47 中显示的那样在四个角落放置图像,你将需要四次列出同一张图像:

background-image: url(i/box-red.gif), url(i/box-red.gif),
                  url(i/box-red.gif), url(i/box-red.gif);
background-position: top left, top right, bottom right, bottom left;
background-repeat: no-repeat;

css5 0847

图 8-47. 将相同的图像放置在四个角落

创建盒子阴影

你已经了解了边框图像、轮廓和背景图像。CSS 还有另一个属性,可以在不影响盒模型的情况下装饰元素的内部和外部:box-shadow

在一个主要关注背景的章节中讨论阴影似乎有些不合时宜,但稍后您将理解我们的理由。

让我们考虑一个简单的框投影阴影:一个距离元素框向下和向右各 10 像素,并且是半不透明的黑色。在它后面,我们将在<body>元素上放置一个重复的背景。所有这些都在图 8-48 中有所说明,并使用以下代码创建:

#box {background: silver; border: medium solid;
     box-shadow: 10px 10px rgb(0 0 0 / 0.5);}

css5 0848

图 8-48. 一个简单的框投影阴影

我们可以看到<body>的背景通过半不透明(或者您更喜欢的半透明)的投影阴影可见。因为未定义模糊或扩展距离,所以投影阴影确实完全仿效了元素框本身的外形——至少表面上是这样的。

这只是看上去仿效盒子形状的原因,因为阴影只在元素的外边框边缘之外可见。我们在前面的图中实际上看不到这一点,因为元素有不透明的背景。你可能只是假设阴影延伸到元素的整个下方,但实际上并非如此。请考虑以下内容,如图 8-49 所示:

#box {background: transparent; border: thin dashed;
     box-shadow: 10px 10px rgb(0 0 0 / 0.5);}

css5 0849

图 8-49. 盒子阴影是不完整的

看起来好像元素的内容(以及填充和边框)区域“挖空”了阴影的一部分。事实上,仅仅是因为阴影从未在那里绘制,这是由规范中盒子阴影定义的方式决定的。正如图 8-49 所示,这意味着任何位于具有投影阴影的框后面的背景都可以通过元素本身看到。这种与背景和边框的(可能看起来有些奇怪的)交互是为什么在这里介绍box-shadow,而不是在文本的早些时候。

到目前为止,我们已经看到使用两个长度值定义的框投影阴影。第一个定义了水平偏移量,第二个定义了垂直偏移量。正数使阴影向下和向右移动,负数使阴影向上和向左移动。

如果给出第三个长度,它定义了模糊距离,这决定了模糊给出多少空间。第四个长度定义了扩展距离,它改变了阴影的大小。正长度值使阴影在模糊发生之前扩展;负值使阴影收缩。以下是图 8-50 中显示的结果:

.box:nth-of-type(1) {box-shadow: 1em 1em 2px rgba(0,0,0,0.5);}
.box:nth-of-type(2) {box-shadow: 2em 0.5em 0.25em rgba(128,0,0,0.5);}
.box:nth-of-type(3) {box-shadow: 0.5em 2ch 1vw 13px rgba(0,128,0,0.5);}
.box:nth-of-type(4) {box-shadow: -10px 25px 5px -5px rgba(0,128,128,0.5);}
.box:nth-of-type(5) {box-shadow: 0.67em 1.33em 0 -0.1em rgba(0,0,0,0.5);}
.box:nth-of-type(6) {box-shadow: 0.67em 1.33em 0.2em -0.1em rgba(0,0,0,0.5);}
.box:nth-of-type(7) {box-shadow: 0 0 2ch 2ch rgba(128,128,0,0.5);}

css5 0850

图 8-50. 各种模糊和扩展阴影

您可能已经注意到一些框具有圆角(通过border-radius),并且它们的阴影也是弯曲的以匹配。这是定义的行为,幸运的是。

我们尚未涵盖box-shadow的一个方面,即inset关键字。如果在box-shadow的值中添加inset,则阴影会渲染在盒子内部,就好像盒子是画布上的凹坑,而不是悬浮在其上(视觉上)。让我们重新使用前面的示例集,并使用插入阴影进行重新设置。这将导致图 8-51 中显示的结果:

.box:nth-of-type(1) {box-shadow: inset 1em 1em 2px rgba(0,0,0,0.5);}
.box:nth-of-type(2) {box-shadow: inset 2em 0.5em 0.25em rgba(128,0,0,0.5);}
.box:nth-of-type(3) {box-shadow: 0.5em 2ch 1vw 13px rgba(0,128,0,0.5) inset;}
.box:nth-of-type(4) {box-shadow: inset -10px 25px 5px -5px  rgba(0,128,128,0.5);}
.box:nth-of-type(5) {box-shadow: 0.67em 1.33em 0 -0.1em rgba(0,0,0,0.5) inset;}
.box:nth-of-type(6) {box-shadow:
   inset 0.67em 1.33em 0.2em -0.1em rgba(0,0,0,0.5);}
.box:nth-of-type(7) {box-shadow: 0 0 2ch 2ch rgba(128,128,0,0.5) inset;}

css5 0851

图 8-51. 各种插入阴影

请注意,inset关键字可以出现在值的开头或结尾,但可以出现在长度和颜色的中间。像0 0 0.1em inset gray这样的值将被视为无效,因为inset关键字的位置不正确。

最后需要注意的一点是,您可以向元素应用任意数量的逗号分隔盒阴影,就像文本阴影一样。有些可以是插入的,有些可以是外部的。以下规则仅仅是无限可能性中的两个:

#shadowbox {
	padding: 20px;
	box-shadow: inset 0 -3em 3em rgb(0 0 0 /0.1),
		0 0 0 2px rgb(255 255 255),
		0.3em 0.3em 1em rgb(0 0 0 / 0.3);}
#wacky {box-shadow: inset 10px 2vh 0.77em 1ch red,
     1cm 1in 0 -1px cyan inset,
     2ch 3ch 0.5ch hsl(117, 100%, 50% / 0.343),
     -2ch -3ch 0.5ch hsl(297, 100%, 50% / 0.23);}

多个阴影按照从后到前的顺序绘制,就像背景图层一样,因此逗号分隔列表中的第一个阴影将位于所有其他阴影的“顶部”。考虑以下情况:

box-shadow: 0 0 0 5px red,
            0 0 0 10px blue,
            0 0 0 15px green;

首先绘制绿色,然后在绿色上方绘制蓝色,最后绘制红色,位于蓝色之上。虽然盒子阴影可以无限宽,但它们不会影响盒模型并且不占用空间。因此,请确保留足够的空间,特别是在进行大偏移或模糊距离时。

提示

filter属性是创建元素阴影的另一种方法,虽然它在行为上更接近text-shadow而不是box-shadow,但应用于整个元素框和文本。详见第二十章。

总结

向元素添加背景(无论是颜色还是图像),可以极大地控制总体视觉呈现效果。与旧方法相比,CSS 的优势在于可以将颜色和背景应用于文档中的任何元素,并以令人惊讶的复杂方式进行操作。

第九章:渐变

CSS 定义的三种图像类型完全由 CSS 描述:线性渐变、径向渐变和锥形渐变。每种类型有两种子类型:重复和非重复。渐变通常用于背景中,尽管它们可以用在允许图像的任何上下文中,例如 list-style-imageborder-image

渐变 是从一种颜色到另一种颜色的视觉过渡。从黄色到红色的渐变将从黄色开始,经过逐渐减少的黄色、更红的橙色阴影,最终到达完全的红色。过渡的平缓程度取决于渐变的空间量以及定义颜色停止点和进度颜色提示的方式。如果你在 100 像素上从白色渐变到黑色,那么默认渐变过程中每个像素将变为更黑的灰色,如图 9-1 所示。

css5 0901

图 9-1。简单渐变的过程

当我们探索渐变的过程时,始终牢记一点:渐变是图像。不管你是通过键入 CSS 描述它们,它们与 SVG、PNG、JPEG 等图像一样——但渐变具有出色的渲染性能,无需额外的 HTTP 请求加载,并且可以无限缩放。

渐变的有趣之处在于它们没有固有的尺寸。如果使用 background-size 属性值 auto,它将被视为 100%。因此,如果你未为背景渐变定义 background-size,它将设置为默认值 auto,这与声明 100% 100% 相同。因此,默认情况下,背景渐变填充整个背景定位区域。只需注意,如果使用长度(而不是百分比)值偏移渐变的背景位置,默认情况下它将平铺。

线性渐变

线性渐变 是沿线性向量进行的渐变填充,称为 渐变线。这里是几个相对简单的渐变示例,结果显示在图 9-2 中:

#ex01 {background-image: linear-gradient(purple, gold);}
#ex02 {background-image: linear-gradient(90deg, purple, gold);}
#ex03 {background-image: linear-gradient(to left, purple, gold);}
#ex04 {background-image: linear-gradient(-135deg, purple, gold, navy);}
#ex05 {background-image: linear-gradient(to bottom left, purple, gold, navy);}

css5 0902

图 9-2。简单线性渐变

这是最基本的渐变形式:两种颜色。这导致了从背景绘制区域顶部的第一种颜色到底部的第二种颜色的渐变。

默认情况下,渐变从顶部到底部,因为渐变的默认方向是 to bottom,这等同于 180deg 和其各种等效方式(例如 0.5turn)。如果你想沿不同方向进行渐变,可以在渐变值前加上一个方向。这就是我们在图 9-2 中展示的所有其他渐变所做的。

渐变必须至少有两个颜色停止点。它们可以是相同的颜色。如果你只想在内容的部分区域背后使用单一颜色,可以使用同一颜色声明两次的渐变,以及背景大小和不重复,如图 9-3 所示:

blockquote {
	padding: 0.5em 1em 2em;
	background-image:
		linear-gradient(palegoldenrod, palegoldenrod),
		linear-gradient(salmon, salmon);
	background-size: 75% 90%;
	background-position: 0px 0px, 15px 30px;
	background-repeat: no-repeat;
     columns: 3;
}

css5 0903

图 9-3. 单一颜色渐变

线性渐变的基本语法如下所示:

linear-gradient(
    [[ <*angle*> | to <*side-or-quadrant*> ],]?
     [ <*color-stop-list*> [, <*color-hint*>]? ]# ,
    <*color-stop-list*>
)

我们将很快探讨颜色停止列表和颜色提示。现在,请记住的基本模式是在开头可选的方向,一系列颜色停止点和/或颜色提示,以及一个结束的颜色停止点。如前所示,linear-gradient()值必须至少有两个颜色停止点。

当你只描述使用诸如topright这样的关键字的一侧或象限时,你只使用to关键字,而你给出的方向总是描述渐变线条指向的方向。换句话说,linear-gradient(0deg,red,green)会使红色在底部,绿色在顶部,因为渐变线条指向 0 度(元素的顶部),因此以绿色结束。虽然它确实“朝向 0 度”,但如果你使用角度值,记得忽略to关键字,因为像to 45deg这样的内容是无效的,并且会被忽略。度数从顶部顺时针增加。

重要的一点是,虽然0deg等同于to top,但45%并不等同于to top right。这在“理解渐变线条:详细信息”中有解释。同样重要的是要记住,使用角度时,无论是度数、弧度还是圈数,单位类型是必需的。0值是无效的,并将阻止创建任何渐变,而0deg是有效的。

设置渐变颜色

在渐变中可以使用任何你喜欢的颜色值,包括像rgba()这样的 alpha 通道值和像transparent这样的关键字。因此,通过与透明度为零的颜色混合,完全可以使渐变中的部分逐渐消失或出现。考虑以下规则,这些规则在图 9-4 中有所描述:

#ex01 {background-image:
    linear-gradient( to right, rgb(200,200,200), rgb(255,255,255) );}
#ex02 {background-image:
    linear-gradient( to right, rgba(200,200,200,1), rgba(200,200,200,0) );}

css5 0904

图 9-4. 淡出至白色与淡出至透明

第一个示例从浅灰色渐变至白色,而第二个示例将相同的浅灰色从不透明渐变至透明,因此允许父元素的黄色背景显示出来。

你不仅限于两种颜色。尽管这是允许的最少颜色数,但你可以添加尽可能多的颜色。考虑以下渐变:

#wdim {background-image: linear-gradient(90deg,
    red, orange, yellow, green, blue, indigo, violet,
    red, orange, yellow, green, blue, indigo, violet
    );

渐变线指向 90 度,即右侧。总共有 14 个颜色停止点,每个逗号分隔的颜色名称对应一个,它们默认均匀分布在渐变线上,第一个位于线的起始处,最后一个位于末尾。默认情况下,在颜色停止点之间,颜色会尽可能平滑地混合。这在图 9-5 中显示,额外的标签指示了颜色停止点在渐变线上的位置。

css5 0905

图 9-5. 沿着渐变线的颜色停止点分布

因此,如果没有指示颜色停止点应该放置在何处,它们将均匀分布。幸运的是,我们可以为每种颜色提供最多两个位置,并且可以使用颜色提示来更好地控制渐变的进展,从而改善视觉效果。

定位颜色停止点

<color-stop> 的完整语法如下:

[<*color*>] [ <*length*> | <*percentage*> ]{1,2}?

在每个颜色值之后,您可以(但不一定要)提供一个或两个位置值。这使您能够将颜色停止点的默认均匀分布进度扭曲为其他形式。

我们将从长度开始,因为它们非常简单。让我们以彩虹的形式进行进展(这次只有一个彩虹),并且每 25 像素出现一种彩虹颜色,如图 9-6 所示:

#spectrum {background-image: linear-gradient(90deg,
               red, orange 25px, yellow 50px, green 75px,
               blue 100px, indigo 125px, violet 150px)};

css5 0906

图 9-6. 每 25 像素放置一个颜色停止点

这个处理得很好,但请注意在 150 像素后会发生什么—紫罗兰色会持续到渐变线的末端。如果设置颜色停止点使其无法达到基本渐变线的末端,就会发生这种情况:最后一个颜色会持续向前。

相反,如果您的颜色停止点超出了基本渐变线的末端,渐变将似乎在它达到渐变线可见部分末端时停止。这在图 9-7 中有所说明,由以下代码创建:

#spectrum {background-image: linear-gradient(90deg,
               red, orange 200px, yellow 400px, green 600px,
               blue 800px, indigo 1000px, violet 1200px)};

css5 0907

图 9-7. 当颜色停止点超出范围时的渐变剪切

由于最后一个颜色停止点在 1200 像素处,但背景大小远远不及这么宽,渐变的可见部分在大约蓝色处停止。

在上述两个示例和图中,请注意第一个颜色(red)没有长度值。如果第一个颜色没有位置,则假定它是渐变线的起始点,就好像声明了0%(或其他零值,如0px)。同样,如果最后一个颜色停止点没有指定位置,则假定它是渐变线的末尾。(但请注意,对于重复渐变来说并非如此,我们将在“重复线性渐变”中讨论。)

您可以使用任何长度值,不仅限于像素,还有 em、视口单位等。您甚至可以在同一个渐变中混合不同的单位,尽管通常不建议这样做,原因稍后我们将详细讨论。如果需要,还可以使用负长度值;这将在渐变线的开始之前放置一个颜色停止点,所有的颜色过渡将按预期进行,并且剪切方式与发生在线末端的剪切方式相同。例如,以下代码的结果是 图 9-8:

#spectrum {background-image: linear-gradient(90deg,
               red -200px, orange 200px, yellow 400px, green 600px,
               blue 800px, indigo 1000px, violet 1200px)};

css5 0908

图 9-8. 当颜色停止点具有负位置时的渐变裁剪

至于百分比,它们是相对于渐变线总长度计算的。在渐变线的中点处放置的颜色停止点为 50%。让我们回到我们的彩虹示例,不再每 25 像素设置一个颜色停止点,而是每个渐变线长度的 10% 设置一个。这将看起来像以下示例,并在 图 9-9 中显示结果:

#spectrum {background-image: linear-gradient(90deg,
    red, orange 10%, yellow 20%, green 30%, blue 40%, indigo 50%, violet 60%)};

css5 0909

图 9-9. 每 10% 放置颜色停止

正如你之前所见,由于最后的颜色停止位于渐变线的末尾之前,其颜色(violet)将一直延续到渐变的末端。这些停止点比之前 25 像素的示例更分散,但除此之外,其他事情基本上是一样的。

如果某些颜色停止点具有位置值,而其他颜色停止点没有,则没有位置值的停止点将在具有位置值的停止点之间均匀分布。以下是等效的:

#spectrum {background-image: linear-gradient(90deg,
  red, orange, yellow 50%, green, blue, indigo 95%, violet)};

#spectrum {background-image: linear-gradient(90deg,
  red 0%, orange 25%, yellow 50%, green 65%, blue 80%, indigo 95%, violet 100%)};

因为 redviolet 没有指定的位置值,它们分别被认为是 0%100%。这意味着 orangegreenblue 将均匀分布在它们两侧明确定义的位置之间。

对于 orange,这意味着位于 red 0%yellow 50% 中间的点,即 25%。对于 greenblue,它们需要排列在 yellow 50%indigo 95% 之间。这是一个 45% 的差距,被分成三段,因为四个值之间有三个间隔。这意味着 65% 和 80%。

您可能会想知道,如果将两个颜色停止点放在完全相同的位置,会发生什么,如下所示:

#spectrum {background-image: linear-gradient(90deg,
    red 0%, orange, yellow 50%, green 50%, blue , indigo, violet)};

所有发生的就是两个颜色停止点被放在一起。图 9-10 显示了结果。

css5 0910

图 9-10. 重合或“硬”颜色停止的效果

渐变线在整个渐变线上像往常一样混合,但在 50% 的位置,它立即在零长度内从黄色到绿色混合,创建了通常称为颜色停止的效果。因此,渐变从 25% 处的橙色(0% 到 50% 之间的一半)到 50% 处的黄色混合,然后在零长度内从黄色到绿色混合,然后在 50% 处从绿色到蓝色混合到 66.67% 处(50% 到 100% 之间的三分之一)。

如果你想创建条纹效果,这种硬停止效果可能会很有用。以下代码会产生图 9-11 中显示的条纹效果:

.stripes {background-image: linear-gradient(90deg,
    gray 0%, gray 25%,
    transparent 25%, transparent 50%,
    gray 50%, gray 75%,
    transparent 75%, transparent 100%);}

css5 0911

图 9-11. 硬停止条纹

也可以通过更简单和更易读的方式来实现这种效果,即为每种颜色指定起始和结束停止位置。以下是如何做到这一点,与图 9-11 中显示的完全相同的结果:

.stripes {background-image:
	linear-gradient(90deg,
		gray 0% 25%,
		transparent 25% 50%,
		gray 50% 75%,
		transparent 75% 100%);}

请注意,0%100%可以被省略,浏览器会自动推断。因此,你可以保留它们以便清晰,或者根据需要删除以提高效率。

在单个渐变中混合两个停止条纹和一个停止颜色点也是可以的。如果你想让渐变的第一和最后一个四分之一是实心灰色条纹,并在它们之间通过透明度过渡,可能会看起来像这样:

.stripes {background-image:
	linear-gradient(90deg,
		gray 0% 25%,
		transparent 50%,
		gray 75% 100%);}

好的,所以如果你把颜色停止点放在一起会发生什么,但如果你把一个放在另一个之前会发生什么呢?就像这样:

#spectrum {background-image: linear-gradient(90deg,
    red 0%, orange, yellow, green 50%, blue 40%, indigo, violet)};

有问题的颜色停止点(在本例中为蓝色)设置为前一个颜色停止点的最大指定值。在这里,它设置为50%,因为它之前的停止点具有该位置。这会创建一个硬停止,我们得到了之前看到的效果,当绿色和蓝色的颜色停止点放在一起时。

这里的关键点是颜色停止点设置为其前一个停止点的最大指定位置。因此,以下两个渐变在视觉上是相同的,因为第一个中的靛蓝颜色停止点被设置为50%

#spectrum {background-image: linear-gradient(90deg,
    red 0%, orange, yellow 50%, green, blue, indigo 33%, violet)};

#spectrum {background-image: linear-gradient(90deg,
    red 0%, orange, yellow 50%, indigo 50%, violet)};

在这种情况下,靛蓝停止点之前的最大指定位置是黄色停止点处的50%。因此,渐变从红色渐变到橙色再到黄色,然后在渐变从靛蓝到紫色之前有一个硬切换。绿色和蓝色并没有被跳过;相反,渐变在零距离内从黄色过渡到绿色再到蓝色再到靛蓝。查看图 9-12 以查看结果。

css5 0912

图 9-12. 处理位置不正确的颜色停止点

这种行为是单个渐变中混合单位通常不鼓励的原因。例如,如果你混合rem单位和百分比,那么使用百分比定位的颜色停止点可能会在使用 rem 定位的较早颜色停止点之前。

设置颜色提示

到目前为止,我们已经使用了颜色停止点,但你可能还记得线性渐变的语法允许在每个颜色停止点之后添加颜色提示:

linear-gradient(
    [[ <*angle*> | to <*side-or-quadrant*> ],]?
     [ <*color-stop-list*> [, <*color-hint*>]? ]# ,
    <*color-stop-list*>
)

<颜色提示>是一种修改两侧颜色停止点之间混合的方式。默认情况下,从一个颜色停止点到下一个的混合是线性的,混合的中点位于两个颜色停止点之间的中间位置,即 50%。它不一定那么简单。以下两个渐变是相同的,并且具有图 9-13 中显示的结果:

linear-gradient(
    to right, rgb(0% 0% 0%) 25%, rgb(90% 90% 90%) 75%
)
linear-gradient(
    to right, rgb(0% 0% 0%) 25%, 50%, rgb(90% ,90% ,90%) 75%
)

css5 0913

图 9-13. 从一个颜色停止点线性混合到下一个

使用颜色提示,我们可以改变进度的中点。不再在中途点达到rgb(45% 45% 45%),而是可以设置为两个停止点之间的任何点。因此,以下 CSS 导致了图 9-14 中看到的结果:

#ex01 {background:
     linear-gradient(to right, rgb(0% 0% 0%) 25%, rgb(90% 90% 90%) 75%);}
#ex02 {background:
     linear-gradient(to right, rgb(0% 0% 0%) 25%, 33%, rgb(90 90% 90%) 75%);}
#ex03 {background:
     linear-gradient(to right, rgb(0% 0% 0%) 25%, 67%, rgb(90% 90% 90%) 75%);}
#ex04 {background:
     linear-gradient(to right, rgb(0% 0% 0%) 25%, 25%, rgb(90% 90% 90%) 75%);}
#ex05 {background:
     linear-gradient(to right, rgb(0% 90% 90%) 25%, 75%, rgb(90% 90% 90%) 75%);}

css5 0914

图 9-14. 从黑色到灰色,具有不同中点提示的情况

在所有五个示例中,第一个颜色停止点位于 25%标记处,最后一个位于 75%标记处,但每个渐变的中点不同。在第一种情况(#ex01)中,使用了默认的线性进度,中间颜色(45%黑色)出现在两个颜色停止点之间的中点处。

在第二种情况(#ex02)中,中间颜色发生在渐变线的 33%点。因此,第一个颜色停止点位于线的 25%点处,中间颜色发生在 33%处,第二个颜色停止点位于 75%处。

在第三个示例(#ex03)中,中点位于渐变线的 67%处;因此,颜色从 25%处的黑色淡出到 67%处的中间颜色,然后从 67%处的中间颜色淡出到 75%处的浅灰色。

第四个和第五个示例展示了当将颜色提示的距离放置在其中一个颜色停止点上时会发生什么:会得到一个硬停止。

关于颜色提示的有趣之处在于,从颜色停止点到颜色提示再到颜色停止点的进度不仅仅是两个线性进度的集合。相反,进度中有一些“弯曲”,以便从颜色提示的一侧到另一侧缓解。(确切的曲线是对数曲线,并基于 Photoshop 使用的渐变进度方程。)通过比较看起来应该是但实际上不是的两个渐变,这一点最容易看出来。正如您可以在图 9-15 中看到的那样,这两个示例的结果非常不同:

#ex01 {background:
    linear-gradient(to right,
        rgb(0% 0% 0%) 25%,
        rgb(45% 45% 45%) 67%,   /* this is a color stop */
        rgb(90% 90% 90%) 75%);}
#ex02 {background:
    linear-gradient(to right,
        rgb(0% 0% 0%) 25%,
        67%,                    /* this is a color hint */
        rgb(90% 90% 90%) 75%);}

css5 0915

图 9-15. 比较两个线性渐变和一个带有提示的过渡

注意灰色渐变在这两个示例中的差异。第一个示例显示了从黑色线性渐变到rgb(45%,45%,45%),然后从那里线性渐变到rgb(90%,90%,90%)。第二个示例在相同的距离上从黑色渐变到浅灰色,渐变的颜色提示点位于 67%处,但是为了尝试更平滑的总体渐变而进行了修改。在两个示例中,25%、67%和 75%处的颜色相同,但是因为 CSS 规范中定义的(有些复杂的)缓解算法,沿途的所有其他色调都不同。

警告

如果你熟悉动画,你可能会考虑将缓动函数(如ease-in)放入颜色提示中,以更好地控制颜色的混合方式。虽然浏览器在某种程度上确实这样做了,正如图 9-15 所示,但截至 2022 年末,开发者目前无法控制这一点(尽管 CSS 工作组正在认真讨论这一能力)。

理解梯度线:详细内容

现在你已经掌握了放置颜色停止的基础知识,让我们仔细看看梯度线是如何构建的,以及它们如何产生它们的效果。首先,让我们设置一个简单的梯度,然后解析它的工作原理:

linear-gradient(
    55deg, #4097FF, #FFBE00, #4097FF
)

现在,这个在罗盘上 55 度处的一维构造如何创建二维渐变填充?首先,放置梯度线并确定其起点和终点。这在图 9-16 中有示意图,并且旁边展示了最终的梯度。

css5 0916

图 9-16. 梯度线的放置和大小

首先要明确的一点是,这里看到的框不是一个元素——它本身就是线性渐变图像。(记住,我们在这里创建的是图像。)该图像的大小和形状可能取决于很多因素,无论是元素背景的大小还是如background-size属性的应用,这是我们稍后会讨论的话题。现在,我们只集中在图像本身上。

所以,在图 9-16 中,你可以看到梯度线直接穿过图像的中心。梯度线总是穿过梯度图像的中心,在这种情况下,梯度图像位于背景区域的中心。(使用background-position来移动梯度图像的位置可能会使看起来梯度的中心不在图像中心,但实际上是在中心。)这个梯度设置为 55 度角,因此指向罗盘上的 55 度。有趣的是梯度线的起点和终点实际上在图像外部。

让我们先谈谈起点。这是在梯度线上的一个点,与梯度线垂直相交于离梯度线方向(55deg)最远的图像角落处。相反,梯度线的终点是在梯度线上的一个点,与离梯度线方向最近的图像角落相交的点。

请注意,“起点”和“终点”这些术语有些误导——梯度线实际上并不在任何一个点停止。事实上,梯度线是无限的。然而,“起点”是默认情况下第一个颜色停止的位置,对应于位置值0%。类似地,“终点”对应于位置值100%

因此,让我们考虑之前定义的渐变:

linear-gradient(
    55deg, #4097FF, #FFBE00, #4097FF
)

起始点的颜色将为#4097FF,中点的颜色(也是渐变图像的中心)将为#FFBE00,结束点的颜色将为#4097FF,在它们之间进行平滑混合。这在图 9-17 中有所说明。

css5 0917

图 9-17. 沿着渐变线计算颜色

到目前为止一切都很好。但是,你可能会想知道,如果这些点在图像之外,底部左侧和顶部右侧的角是如何设置为与起始点和结束点计算出的相同的蓝色的?因为沿着渐变线的每个点的颜色都是从渐变线垂直延伸出来的。这在图 9-18 中部分显示,通过在起始点和结束点以及它们之间每 5%的渐变线处延伸垂直线来展示。请注意,垂直于渐变线的每条线都是单色的。

css5 0918

图 9-18. 沿着渐变线延伸的选定颜色

希望这足以让你在脑海中填补其余部分,所以让我们考虑在各种其他设置中渐变图像会发生什么。我们将使用与之前相同的渐变定义,但这次应用于宽、正方形和高图像。这些显示在图 9-19 中。请注意,起始点和结束点的颜色始终会进入渐变图像的角落。

css5 0919

图 9-19. 如何为各种图像构建渐变

请注意,我们非常仔细地说“起始点和结束点的颜色”,而没有说“起始和结束颜色”。这是因为,正如您之前看到的,颜色停止点可以放在起始点之前和结束点之后,如下所示:

linear-gradient(
    55deg, #4097FF -25%, #FFBE00, #4097FF 125%
)

这些颜色停止点的放置,起始点和结束点,沿着渐变线计算颜色的方式以及最终渐变都显示在图 9-20 中。

css5 0920

图 9-20. 具有超出起始点和结束点的停止点的渐变

再次看到,底部左侧和顶部右侧的颜色与起始点和结束点的颜色相匹配。只是在这种情况下,由于第一个颜色停止点在起始点之前,起始点的实际颜色是第一个和第二个颜色停止点的混合。同样适用于结束点,它是第二个和第三个颜色停止点的混合。

现在事情有点疯狂了。记得你可以使用方向关键字,比如topright,来指示渐变线的方向吗?假设你希望渐变线朝右上方,那么你可以创建一个如下的渐变图像:

linear-gradient(
    to top right, #4097FF -25%, #FFBE00, #4097FF 125%
)

这并会导致渐变线与右上角相交。如果只是这样该多好!相反,发生的事情要复杂得多。首先,让我们在图 9-21(#cab_fig78)中画出来,这样我们就有东西可以参考了。

css5 0921

图 9-21。朝右上方向的渐变

你的眼睛没有被欺骗:渐变线远离了右上角。不过,它确实是朝着图像的右上象限前进。这才是to top right真正的意思:朝着图像的右上象限前进,而不是朝向右上角。

如图 9-21(#cab_fig78)所示,要确切了解这意味着什么,需要做以下事情:

  1. 从图像的中点向已声明的象限中的相邻角绘制一条线。因此,对于右上象限,相邻的角是左上角和右下角。

  2. 找到该线的中心点,即图像的中心点,并通过该线的垂直线绘制渐变线,通过中心点指向已声明的象限。

  3. 构建渐变——即确定起点和终点、放置或分布沿渐变线的颜色停止点,然后按照通常的方式计算整个渐变图像。

这个过程有一些有趣的副作用。首先,中点处的颜色将始终从一个象限相邻的角延伸到另一个象限相邻的角。其次,如果图像的形状发生变化,即其纵横比发生变化,则渐变线也将重置其方向,稍微重新定位以适应新的纵横比。因此,如果你有可变元素,请注意这一点。第三,一个完全正方形的渐变图像将具有与角相交的渐变线。图 9-22(#cab_fig79)展示了这三种副作用的示例,使用以下渐变定义:

linear-gradient(
    to top right, purple, green 49.5%, black 50%, green 50.5%, gold
)

css5 0922

图 9-22。象限定向渐变的副作用示例

遗憾的是,没有办法说“将渐变线指向非正方形图像的角”,除非自己计算所需的度数,并明确声明,这个过程很可能需要使用 JavaScript,除非你知道图像在所有情况下将永远是精确的尺寸。(或使用aspect-ratio属性;详见第六章)

尽管线性渐变沿着由角度设定的渐变线方向进行渐变,但也可以创建镜像渐变;关于这一点,很奇怪地,请参阅“径向渐变”。

重复线性渐变

默认情况下,常规渐变是自动调整大小的,与其应用的背景区域的大小匹配。换句话说,默认情况下,渐变图像占据了所有可用的背景空间,并且不重复。

故意设置背景大小和平铺图像,特别是使用硬颜色停止,可以产生有趣的效果。通过声明两个线性渐变背景图像,使用硬颜色停止,垂直渐变线,并不同的背景颜色,你可以通过设置一些渐变图像、平铺它们,然后在下面放置一种颜色,为任何摆设创造野餐桌布效果,如 图 9-23 所示:

div {
   background-image:
      linear-gradient(to top, transparent 1vw, rgba(0 0 0 / 0.2) 1vw),
      linear-gradient(to right, transparent 1vw, rgba(0 0 0 / 0.2) 1vw);
   background-size: 2vw 2vw;
   background-repeat: repeat;
}
div.fruit {background-color: papayawhip;}
div.grain {background-color: palegoldenrod;}
div.fishy {background-color: salmon;}

css5 0923

图 9-23. Papayawhip、palegoldenrod 和 salmon 颜色的桌布

而不是用 background-size 定义渐变大小并使用 background-repeat 平铺它,我们可以使用重复线性渐变语法。通过在线性渐变前添加 repeating,它们将在渐变的尺寸内无限重复。换句话说,因为使用 repeating-linear-gradient 时渐变线的大小是最后一个颜色停止位置减去第一个颜色停止位置(在本例中为 2vw),所以我们可以移除尺寸和重复属性,如下所示,得到与 图 9-23 中显示的相同结果:

div {
   background-image:
     repeating-linear-gradient(to top,
        transparent 0 1vw, rgb(0 0 0 / 0.2) 1vw 2vw),
     repeating-linear-gradient(to right,
        transparent 0 1vw, rgb(0 0 0 / 0.2) 1vw 2vw);
}
div.fruit {background-color: papayawhip;}
div.grain {background-color: palegoldenrod;}
div.fishy {background-color: salmon;}

对于像这些桌布这样的简单图案来说很好用,但对于更复杂的情况来说尤为方便。例如,如果你声明以下不重复的渐变,你会在图像重复的地方出现不连续,如图 9-24 所示:

h1.example {background:
    linear-gradient(-45deg, black 0, black 25px, yellow 25px, yellow 50px)
    top left/40px 40px repeat;}

css5 0924

图 9-24. 使用重复背景图像平铺渐变图像

可以尝试准确地锁定元素和渐变图像的大小,然后搞乱渐变图像的构造,试图使边缘对齐,但使用以下方法会更容易些,其结果如 图 9-25 所示:

h1.example {background: repeating-linear-gradient(-45deg,
        black 0 25px, yellow 25px 50px) top left;}

css5 0925

图 9-25. 重复渐变图像

注意,最后一个颜色停止以显式长度 (50px) 结束。这对于重复渐变非常重要,因为最后一个颜色停止的长度值定义了图案的总长度。如果你省略了结束停止,它将默认为 100%,即渐变线的末端。

如果你正在使用更平滑的过渡效果,你需要注意最后一个颜色停止处的颜色值要与第一个颜色停止处的颜色值匹配。考虑以下情况:

repeating-linear-gradient(-45deg, purple 0px, gold 50px)

这将在 50 像素处产生从紫色到金色的平滑渐变,然后在另外 50 像素处回到紫色,并再次进行紫色到金色的混合。通过添加一个与第一个颜色停止处相同颜色的额外颜色停止,可以使渐变平滑,避免硬停止线:

repeating-linear-gradient(-45deg, purple 0px, gold 50px, purple 100px)

查看 图 9-26 对比这两种方法。

css5 0926

图 9-26. 处理重复渐变图像中的硬重置

您可能已经注意到,到目前为止,所有重复渐变都没有定义大小。这意味着图像默认大小为应用到它们的元素的完整背景定位区域,默认行为适用于没有固有高度和宽度的图像。

如果你使用background-size来调整重复渐变图片的大小,那么渐变将仅在渐变图片的边界内重复。然后,如果你使用background-repeat重复该图片,你可能会再次出现背景中的不连续性。

如果你在重复线性渐变中使用百分比,它们将会被放置在与非重复渐变相同的位置。然而,这意味着所有由这些颜色停止定义的渐变都将被看到,并且没有重复将是可见的,因此百分比往往是有点无意义的,特别是对于重复线性渐变。

径向渐变

线性渐变非常棒,但有时您确实需要一个圆形渐变。您可以使用这样的渐变来创建聚光灯效果,圆形阴影,圆角发光或任何其他效果,包括反射渐变。使用的语法与线性渐变的语法类似,但也存在一些有趣的差异:

radial-gradient(
    [ [ <*shape*> ‖ <*size*> ] [ at <*position*>]? , | at <*position*>, ]?
      [ <*color-stop-list*> [, <*color-hint*>]? ] [, <*color-stop-list*> ]+
)

这归结为您可以选择声明形状和大小,选择声明渐变中心的位置,然后声明两个或更多颜色停止,以及停止之间的可选颜色提示。在形状和大小部分提供了一些有趣的选项,因此让我们逐步构建这些选项。

首先,让我们看一个简单的径向渐变——事实上是可能的最简单的——呈现在各种不同形状的元素中(图 9-27):

.radial {background-image: radial-gradient(purple, gold);}

css5 0927

图 9-27. 多种设置中的简单径向渐变

在所有这些情况下,因为没有声明位置,所以使用了center的默认值,并且默认椭圆的纵横比与图像大小相同。因为没有声明形状,所以对于所有情况,除了正方形元素,形状都是椭圆。最后,因为没有声明颜色停止或颜色提示位置,所以第一个位于渐变光线的起始位置,最后一个位于结束位置,两者之间线性混合。

没错:渐变光线 是径向渐变中与线性渐变中的渐变线相对应的部分。它从渐变的中心向外延伸,直接到右侧,渐变的其余部分则根据它构建。(我们稍后会详细介绍细节。)

设置形状和大小

首先,径向渐变确实有两种可能的形状值(因此有两种可能的形状):circleellipse。渐变的形状可以明确声明,也可以通过渐变图片的大小来暗示。

因此,关于大小的问题。通常,调整径向渐变大小的最简单方法是使用一个非负长度(如果调整的是圆形)或两个非负长度(如果调整的是椭圆形)。比如,你有这样一个径向渐变:

radial-gradient(50px, purple, gold)

这会创建一个从中心渐变为 50 像素距离处的金色渐变的圆形径向渐变。如果我们增加另一个长度,形状将变成一个宽度与第一个长度相同、高度与第二个长度相同的椭圆:

radial-gradient(50px 100px, purple, gold)

图 9-28 展示了这两种渐变效果。

css5 0928

图 9-28. 简单的径向渐变

请注意,渐变的形状与其出现图像的整体大小和形状无关。如果你使渐变成圆形,它就是一个圆形,即使它在一个矩形渐变图像内部。同样,椭圆形始终保持椭圆形,即使它在一个正方形渐变图像内部。

你也可以使用百分比值来表示大小,但仅限于椭圆。圆形不能使用百分比大小,因为无法指示百分比所应用的轴线。(想象一个高 500 像素宽 100 像素的图像。10%应该是 10 像素还是 50 像素?)如果你尝试为圆形提供百分比值,整个声明都会因为无效值而失败。

如果你给椭圆形提供百分比值,那么和往常一样,第一个值指的是水平轴,第二个值指的是垂直轴。下面的渐变在图 9-29 中展示了各种设置:

radial-gradient(50% 25%, purple, gold)

css5 0929

图 9-29. 百分比大小的椭圆形渐变

当涉及到椭圆形时,你还可以混合使用长度和百分比,只需注意小心。因此,如果你感到有信心,完全可以创建一个椭圆形径向渐变,高 10 像素,宽度为元素宽度的一半,如下所示:

radial-gradient(50% 10px, purple, gold)

恰巧地,长度和百分比不是调整径向渐变大小的唯一方法。除了这些值类型外,还有四个关键词可供调整径向渐变的大小,其效果在此总结:

closest-side

如果径向渐变的形状是圆形,渐变会被调整大小,使渐变射线的末端正好触及最接近径向渐变中心点的渐变图像边缘。如果形状是椭圆,渐变射线的末端会分别触及水平和垂直轴上最接近的边缘。

farthest-side

如果径向渐变的形状是圆形,渐变会被调整大小,使渐变射线的末端正好触及离径向渐变中心点最远的渐变图像边缘。如果形状是椭圆,渐变射线的末端会分别触及水平和垂直轴上的最远边缘。

closest-corner

如果径向渐变的形状是圆形,则渐变被调整大小,使得渐变射线的端点恰好触及离径向渐变中心点最近的渐变图像角落。如果形状是椭圆,则渐变射线的端点仍然触及离中心最近的角落,并且椭圆具有与指定closest-side时相同的长宽比。

farthest-corner(默认)

如果径向渐变的形状是圆形,则渐变被调整大小,使得渐变射线的端点恰好触及离中心点最远的渐变图像角落。如果形状是椭圆,则渐变射线的端点仍然触及离中心最远的角落,并且椭圆具有与指定farthest-side时相同的长宽比。注意:这是径向渐变的默认大小值,因此在未声明任何大小值时使用。

要更好地可视化每个关键字的结果,请参见图 9-30,它展示了每个关键字作为圆形和椭圆应用的效果。

css5 0930

图 9-30. 径向渐变大小关键字的效果(位于at 33% 66%位置)

这些关键字不能与椭圆形径向渐变中的长度或百分比混合使用;因此,closest-side 25px是无效的,会被忽略。

你可能在图 9-30 中注意到的一点是渐变不是从图像的中心开始的。这是因为它们被放置在其他地方,这是下一节的主题。

定位径向渐变

如果您想将径向渐变的中心位置偏离center的默认位置,可以使用任何对于background-position有效的位置值。我们不会在这里复制这种相当复杂的语法;如果需要恢复记忆,请回到“定位背景图像”。

当我们说“任何有效的位置值”时,这意味着任何允许的长度、百分比、关键字等的组合。这还意味着,如果省略其中一个位置值,它将被推断为background-position的相同方式。因此,举一个例子,center等同于center center。径向渐变位置和背景位置之间唯一的主要差异是默认值:对于径向渐变, 默认位置是center,而不是0% 0%

为了让您对可能性有所了解,请考虑以下规则,这些规则在图 9-31 中进行了说明:

radial-gradient(at bottom left, purple, gold);
radial-gradient(at center right, purple, gold);
radial-gradient(at 30px 30px, purple, gold);
radial-gradient(at 25% 66%, purple, gold);
radial-gradient(at 30px 66%, purple, gold);

css5 0931

图 9-31. 更改径向渐变的中心位置

那些定位的径向渐变都没有明确的尺寸,因此它们都默认为farthest-corner。这是对预期默认行为的合理猜测,但并不是唯一的可能性。让我们将一些尺寸混入这些渐变中,看看这如何改变事物(如图 9-32 所示):

radial-gradient(30px at bottom left, purple, gold);
radial-gradient(30px 15px at center right, purple, gold);
radial-gradient(50% 15% at 30px 30px, purple, gold);
radial-gradient(farthest-side at 25% 66%, purple, gold);
radial-gradient(closest-corner at 30px 66%, purple, gold);

css5 0932

图 9-32. 改变明确定义大小的径向渐变的中心位置

很巧妙。现在,假设我们想要比一个颜色到另一个颜色的渐变更复杂的东西。接下来是颜色停止点!

使用径向颜色停止和渐变光线

径向渐变的颜色停止点与线性渐变具有相同的语法并且类似地工作。让我们回到最简单的可能径向渐变,并跟随一个更明确的等价版本:

radial-gradient(purple, gold);
radial-gradient(purple 0%, gold 100%);

因此,渐变光线从中心点向外延伸。在 0%(起始点,也是渐变的中心)处,光线为紫色。在 100%(结束点)处,光线为金色。在两个停止点之间是从紫色平滑过渡到金色;超过结束点则是纯金色。

如果我们在紫色和金色之间添加一个停止点,但不给它一个位置,那么停止点将被放置在两种颜色之间的中间位置,并且混合效果将相应地改变,如图 9-33 所示:

radial-gradient(100px circle at center, purple 0%, green, gold 100%);

css5 0933

图 9-33. 添加一个颜色停止点

如果我们在那里添加green 50%,我们会得到相同的结果,但你明白我的意思。渐变光线的颜色平滑地从紫色过渡到绿色,然后到金色,在光线的那一点之后是纯金色。

这说明了渐变线(用于线性渐变)与渐变光线之间的一个区别:线性渐变是通过沿着每个点垂直延伸颜色来推导的。径向渐变也有类似的行为,不同的是,不是从渐变线上产生的线条,而是创建椭圆;这些椭圆是结束点的椭圆的放大或缩小版本。图 9-34 展示了一个渐变光线以及沿其各点绘制的椭圆。

css5 0934

图 9-34. 渐变光线及其生成的部分椭圆

这提出了一个有趣的问题:每个渐变光线的结束点(如果你愿意,可以是 100%点)如何确定?这是渐变光线与尺寸描述的形状相交的点。对于圆形,这很容易:渐变光线的结束点是尺寸值指示的距离中心的位置。因此,对于25px 圆形渐变,光线的结束点是距离中心 25 像素的位置。

对于椭圆,基本上是同样的操作,只是距离中心的距离取决于椭圆的水平轴。给定一个是40px 20px 椭圆的径向渐变,结束点将从中心向右直接移动 40 像素。图 9-35 详细展示了这一点。

css5 0935

图 9-35. 设置渐变射线的结束点

线性渐变线和径向渐变射线之间的另一个区别是您可以看到超出结束点的内容。您可能会记得,线性渐变线始终是这样绘制的,以便您可以看到 0%和 100%点的颜色,但在它们之外什么也看不到;渐变线永远不能小于渐变图像的最长轴,并且通常会比该轴长。另一方面,对于径向渐变,您可以将径向形状大小调整为小于总渐变图像的大小。在这种情况下,最后一个颜色停止点的颜色将从结束点向外延伸。(在几个先前的图像中,您已经看到了这一点。)

相反,如果设置的颜色停止点超出了射线的结束点,您可能会看到到该停止点的颜色。请参考以下渐变,图 9-36 所示:

radial-gradient(50px circle at center, purple, green, gold 80px)

css5 0936

图 9-36. 超出结束点的颜色停止点

第一个颜色停止点没有位置,因此将其设置为0%,即为中心点。最后一个颜色停止点设置为80px,因此它将在所有方向上距中心点 80 像素。中间的颜色停止点green放置在两者之间(距离中心 40 像素)。因此,我们得到一个渐变,到 80 像素处变成金色,然后在那一点之外继续保持金色。

即使圆明确设置为 50 像素,这种情况仍然会发生。它仍然是 50 像素的半径;只是最后一个颜色停止点的定位使得这一事实变得不太相关。从视觉上讲,我们可以认为声明了这个:

radial-gradient(80px circle at center, purple, green, gold)

或者更简单地,只是这样:

radial-gradient(80px, purple, green, gold)

如果您使用百分比来设置颜色停止点,则相同的行为也适用。从视觉上讲,这些与之前的示例以及彼此等效:

radial-gradient(50px, purple, green, gold 160%)
radial-gradient(80px, purple, green, gold 100%)

那么,如果您为颜色停止点设置一个负位置会发生什么呢?结果与线性渐变线的情况几乎相同:负颜色停止点用于计算起始点的颜色,但在其他情况下不可见。因此,以下渐变将产生图 9-37 所示的结果:

radial-gradient(80px, purple -40px, green, gold)

css5 0937

图 9-37. 处理负色停止位置

鉴于这些颜色停止位置,第一个颜色停止点位于-40px,最后一个位于80px(因为,由于缺乏显式位置,它默认为结束点),而中间位置则位于它们之间的中间位置。结果与我们明确使用此相同:

radial-gradient(80px, purple -40px, green 20px, gold 80px)

这就是为什么渐变中心的颜色是绿紫色的原因:它是三分之一紫色,两分之一绿色的混合。从那里,它继续向绿色混合,然后转向金色。紫绿混合的其余部分,即位于渐变射线的“负空间”上的部分,是不可见的。

处理退化情况

既然我们可以声明径向渐变的大小和位置,问题来了:如果一个圆形渐变的半径为零,或者一个椭圆形渐变的高度或宽度为零会怎样?这些条件并不像你想象的那么难以实现。除了明确声明径向渐变的大小为 0px0%,你还可以像这样做:

radial-gradient(closest-corner circle at top right, purple, gold)

渐变的大小设置为 closest-corner,并且中心已移动到右上角,所以最近的角离中心 0 像素远。现在怎么办?

在这种情况下,规范明确指出渐变应该渲染为“一个半径[为]任意大于零的极小数”。这可能意味着它的半径相当于一个十亿分之一像素,或者皮米,甚至普朗克长度。有趣的是,这意味着渐变仍然是一个圆。只不过是一个非常非常非常小的圆。可能它太小以至于渲染不出可见的东西。如果是这样的话,你将只会得到一个填充为最后一个颜色停止点颜色的纯色填充。

长度为零的椭圆形在定义行为上有着迷人的不同。让我们假设以下情况:

radial-gradient(0px 50% at center, purple, gold)

规范指出,任何宽度为零的椭圆形渲染为“一个宽度[为]任意大的数,高度[为]任意大于零的数”的椭圆形。换句话说,将其渲染为围绕通过椭圆形中心的垂直轴镜像的线性渐变。规范还说,在这种情况下,任何百分比位置的颜色停止点将解析为 0px。这通常会导致一个与最后一个颜色停止点定义的颜色匹配的纯色。

另一方面,如果使用长度来定位颜色停止点,你可以免费获得一个在垂直方向镜像的水平线性渐变。考虑下面的渐变,如图 9-38 所示:

radial-gradient(0px 50% at center, purple 0px, gold 100px)

css5 0938

图 9-38. 零宽度椭圆的效果

这是怎么发生的呢?首先要记住,规范说 0px 的水平宽度被视为一个微小的非零数。为了举例说明,假设是千分之一像素(0.001 px)。这意味着椭圆形的形状是千分之一像素宽,图像高度的一半。再举例说明,假设高度是 100 像素。这意味着第一个椭圆形的形状是千分之一像素宽,100 像素高,其宽高比为 0.001:100,或者 1:100,000。

好了,所以沿着渐变射线绘制的每个椭圆都具有 1:100,000 的宽高比。这意味着在渐变射线的半像素处,椭圆宽度为 1 像素,高度为 100,000 像素。在 1 像素时,宽度为 2 像素,高度为 200,000 像素。在 5 像素时,椭圆为 10 像素宽,一百万像素高。在渐变射线的 50 像素处,椭圆宽度为 100 像素,高度为 1000 万像素。等等。这在图 9-39 中有图示。

css5 0939

图 9-39。非常非常高的椭圆

因此,你可以看到为什么视觉效果是镜像的线性渐变。这些椭圆实际上在绘制垂直线。从技术上讲,它们并不是,但在实际操作中它们是。结果就像你有一个垂直镜像的水平渐变,因为每个椭圆都集中在渐变的中心,并且它的两侧都会被绘制。虽然这可能是一个径向渐变,但我们看不到它的径向特性。

另一方面,如果椭圆有宽度但没有高度,结果会大不相同。你可能认为结果将是沿水平轴镜像的垂直线性渐变,但事实并非如此!相反,结果是一个与最后一个颜色停止点相同的纯色(除非它是重复渐变,这是我们马上要讨论的一个主题,那么它应该是渐变的平均颜色)。因此,根据以下任何一种情况,你将得到一个纯金色:

radial-gradient(50% 0px at center, purple, gold)
radial-gradient(50% 0px at center, purple 0px, gold 100px)

为什么会有这样的差异呢?这要追溯到径向渐变是如何根据渐变射线构建的。再次记住,根据规范,这里的零距离被视为一个非常小的非零数。与以前一样,我们假设0px被重新分配为0.001px,而50%评估为 100 像素。这是一个宽高比为 100:0.001,即 100,000:1。

所以,为了得到一个高度为 1 像素的椭圆,该椭圆的宽度必须为 100,000 像素。但我们的最后一个颜色停止点只在 100 像素处!在那一点上,绘制的椭圆宽度为 100 像素,高度为千分之一像素。整个紫色到金色的过渡必须发生在那千分之一像素的范围内。之后的所有部分都是金色,如最后一个颜色停止点所示。因此,我们只能看到金色。

你可能会认为,如果你将最后一个颜色停止点的位置增加到100000px,你会看到一个薄薄的紫色条纹横穿图像。你是对的,如果浏览器在这些情况下将0px视为0.001px的话。如果它假设的是0.00000001px,那么你必须进一步增加颜色停止点的位置才能看到任何东西。这假设浏览器实际上在计算和绘制所有这些椭圆,而不是只是硬编码特殊情况。老实说,后者更有可能。如果我们掌管浏览器的渐变渲染代码,我们会这样做。

如果椭圆的宽度和高度都为零怎么办?在这种情况下,规范写明将使用零宽度行为;因此,您将获得镜像线性渐变行为。

注意

截至 2022 年底,浏览器对这些边缘情况的定义行为的支持稳定性堪忧。在某些情况下,一些浏览器在所有情况下都使用最后一个颜色停止的颜色,而在其他情况下则拒绝绘制渐变。

重复径向渐变

虽然百分比在重复线性渐变中可以使其成为非重复渐变,但是如果定义了圆形或椭圆的大小、定义了沿渐变射线的百分比位置,并且您可以看到渐变射线的端点之外,百分比可能非常有用。例如,假设以下情况:

.allhail {background:
    repeating-radial-gradient(100px 50px, purple, gold 20%, green 40%,
                              purple 60%, yellow 80%, purple);}

由于有五个颜色停止和大小为 100px,每 20 像素就会出现一个颜色停止,颜色按照声明的模式重复。因为第一个和最后一个颜色停止具有相同的颜色值,所以没有明显的颜色切换。涟漪会无限扩展,或者至少直到超出渐变图像的边缘。请参见图 9-40 作为示例。

css5 0940

图 9-40. 重复径向渐变

想象一下,如果用一个彩虹的重复径向渐变会是什么样子!

.wdim {background:
    repeating-radial-gradient(
        100px circle at bottom center,
        rgb(83%,83%,83%) 50%,
        violet 55%, indigo 60%, blue 65%, green 70%,
        yellow 75%, orange 80%, red 85%,
        rgb(47%,60%,73%) 90%
    );}

在创建重复径向渐变时,请记住以下两点:

  • 如果未为径向渐变声明尺寸维度,则默认为一个椭圆,其高度与宽度比例与整个渐变图像相同;而且,如果未为背景图像使用background-size声明尺寸,则渐变图像将默认为应用于其背景的元素的高度和宽度(或者,如果作为列表样式符号使用,则为浏览器赋予的尺寸)。

  • 默认的径向尺寸值是farthest-corner。这将使渐变射线的端点远离中心点的椭圆,使其与渐变图像的最远角相交。

这些再次强调,提醒您,如果您坚持使用默认值,那么拥有重复渐变实际上没有什么意义,因为您只能看到重复的第一次迭代。只有当您限制渐变的初始尺寸时,重复才会变得可见。

锥形渐变

径向渐变很有趣,但是如果您想要一个环绕中心点的渐变,类似于颜色色调轮,那就是 CSS 称之为锥形渐变,可以看作是一系列同心的线性渐变,弯曲成圆形。从另一个角度看,从中心点到任意距离,都存在一个圆,其外边缘可以用指定的颜色停止的线性渐变来展开。

锥形渐变比描述更容易展示,因此考虑以下 CSS,它在图 9-41 中有所说明,同时还有一个线性图示,展示了各个停止点如何环绕锥形空间:

background:
     conic-gradient(
          black, gray, black, white, black, silver, gray
     );

css5 0941

图 9-41. 简单的锥形渐变及其线性等效图

注意线性渐变上每个颜色停止点的标签:那里列出的带圈数字在锥形渐变中重复出现,以显示每个颜色停止点的位置。在锥形渐变的 60 度处,有一个gray颜色停止点。在 180 度处,有一个white颜色停止点。在锥形渐变的顶部,0deg360deg点相遇,所以blackgray相邻。

默认情况下,锥形渐变从 0 度开始,使用与转换和 CSS 的其他部分相同的罗盘度系统,因此0deg在顶部。如果想从不同的角度开始并环绕圆圈回到该点,只需在conic-gradient值的前面添加from和角度值即可。以下示例都会产生相同的结果:

conic-gradient(from 144deg, black, gray, black, white)
conic-gradient(from 2.513274rad, black, gray, black, white)
conic-gradient(from 0.4turn, black, gray, black, white)

如果锥形渐变给定了不同的起始角度,比如from 45deg,它就会作为整个锥形渐变的旋转。考虑以下两个例子,其结果在图 9-42 中显示:

conic-gradient(black, white 90deg, gray 180deg, black 270deg, white)
conic-gradient(from 45deg, black, white 90deg, gray 180deg, black 270deg, white)

css5 0942

图 9-42. 带有角度颜色停止点和不同起始角度的锥形渐变

不仅起始点旋转了 45 度,所有其他颜色停止点也旋转了。因此,即使第一个颜色停止点的角度是90deg,它实际上出现在 135 度标记处,即 90 度再加上 45 度的旋转。

同样可以像径向渐变一样更改渐变图像中心点的位置。语法非常类似,如下面的代码块所示(在图 9-43 中有说明):

conic-gradient(from 144deg at 3em 6em, black, gray, black, white)
conic-gradient(from 144deg at 67% 25%, black, gray, black, white)
conic-gradient(from 144 deg at center bottom, black, gray, black, white)

css5 0943

图 9-43. 旋转和偏移的锥形渐变

在这三个示例中的第一个示例中,锥形渐变的中心点位于从左上角向右3em,向下6em处。类似地,第二个示例显示了锥形渐变图像中心点位于横向的67%处,垂直向下25%处。

第三个例子展示了当锥形渐变的中心点放置在图像的一条边上时会发生什么情况:我们只能看到半个(最多)渐变。在这种情况下,可见的是顶部一半——也就是从 270 度到 90 度的颜色。

所以总体而言,锥形渐变的语法如下所示:

conic-gradient(
    [ from <*angle*>]? [ at <*position*>]? , | at <*position*>, ]?
      <*color-stop*> , [ <*color-hint*>]? , <*color-stop*> ]+
)

如果没有给定from角度,它默认为0deg。如果没有给定at位置,它默认为50% 50%(即锥形渐变图像的中心)。

就像径向渐变和线性渐变一样,颜色停止点的距离可以用百分比值来指定;在这种情况下,它解析为角度值。因此,对于从 0 度开始的锥形渐变,颜色停止点距离为25%将解析为 90 度,因为 90 度是 360 的 25%。锥形颜色停止点也可以指定为角度值,如前所示。

不能为锥形渐变的颜色停止点距离指定长度值。只有百分比和角度是可接受的,并且它们可以混合使用。

创建锥形颜色停止点

如果你希望锥形渐变在整个圆周从颜色到颜色平滑混合,必须使最后的颜色停止点与第一个颜色停止点匹配。否则,你将会看到早期示例中显示的那种硬过渡效果。如果你想创建一个颜色色调环,例如,你需要像这样声明:

conic-gradient(red, magenta, blue, aqua, lime, yellow, red)

但事实上这并不是一个真正的圆轮,因为锥形渐变图像填充整个背景区域,而 CSS 中的背景区域(到目前为止)默认是矩形的。要使颜色轮看起来像一个真正的颜色轮,你需要使用圆形剪裁路径(参见第二十章)或者在方形元素上圆角处理(参见第七章)。例如,以下代码将产生图 9-44 所示的结果:

.hues {
     height: 10em; width: 10em;
     background: conic-gradient(red, magenta, blue, aqua, lime, yellow, red);
}
#wheel {
     border-radius: 50%;
}
<div class="hues"></div>
<div class="hues" id="wheel"></div>

css5 0944

图 9-44. 具有和不具有圆角处理的色调轮锥形渐变

强调的是,虽然我们很容易将锥形渐变想象成圆形,但最终结果是一个矩形,没有任何剪切或其他使元素背景区域非矩形化的努力。所以,如果你打算用锥形渐变来制作比如说饼图,你必须做的不仅仅是定义一个带有硬停的锥形渐变。

就像我们在线性渐变中使用两个长度百分比值来创建硬停一样,我们可以在锥形渐变中使用两个硬停。例如:

conic-gradient(
	green 37.5%,
	yellow 37.5% 62.5%,
	red 62.5%);

在这种语法中,给定的颜色停止点可以写为<color> <beginning> <ending>,其中<beginning>和<ending>是百分比或角度值。

如果你想在颜色之间创建更平滑的过渡但仍然保持它们大部分是实心的,那么<color> <beginning> <ending> 的语法可以帮助很多。例如,以下锥形渐变使绿色、黄色和红色之间的过渡更加柔和,而不会使整体渐变过于“涂抹”:

conic-gradient(green 35%, yellow 40% 60%, red 65%);

这段代码从 0 到 126 度(35%)渲染了一块绿色的实心楔形,然后在 126 度到 144 度(40%)之间平滑过渡从绿色到黄色,之后从 144 度到 216 度(60%)有一块黄色的实心楔形。类似地,在 216 度到 234 度(65%)之间发生了从黄色到红色的平滑过渡,超过这个范围,有一块红色的实心楔形延续到 360 度。

所有这些都在图 9-45 中有所体现,额外的注释标记了计算角度的位置。

css5 0945

图 9-45. 具有单色楔形和平滑过渡的锥形渐变

正如之前章节中讨论过的野餐桌布那样,这种语法使得使用锥形渐变相对容易重新创建:

background-image: conic-gradient(
	rgba(0 0 0 / 0.2) 0% 25%,
	rgba(0 0 0 / 0.4) 25% 50%,
	rgba(0 0 0 / 0.2) 50% 75%,
	transparent 75% 100%
	);
background-size: 2vw 2vw;
background-repeat: repeat;

这在单个渐变图像中创建了一个四方形的图案集合。然后对该图像进行大小调整和重复。虽然与使用重复线性渐变相比,这并不更有效或更优雅,但它确实体现了一定的巧思,这让我们感到满意。

重复的锥形渐变

现在我们来讲一下重复的锥形渐变,如果你想要创建星芒图案或者像棋盘一样的简单图案,这是非常有用的。例如:

conic-gradient(
    #0002 0 25%, #FFF2 0 50%, #0002 0 75%, #FFF2 0 100%
    )

这样就设置了一个有四个颜色停止但只有两种颜色的棋盘图案。我们可以使用repeating-conic-gradient重新表达这一点,使用新的颜色使图案更加清晰:

repeating-conic-gradient(
     #343 0 25%, #ABC 0 50%
     )

在这种简单的重复情况中,唯一需要设置的是前两个颜色停止点。之后,这些停止点将重复直到填满锥形渐变的完整 360 度,如图 9-46 所示。

css5 0946

图 9-46. 重复的锥形渐变

这意味着我们可以创建任何大小的楔形,具有任何过渡,并在整个锥形圆周上重复它们。这里仅展示了三个示例,渲染在图 9-47 中:

repeating-conic-gradient(#117 5deg, #ABE 15deg, #117 25deg)
repeating-conic-gradient(#117 0 5deg, #ABE 0 15deg, #117 0 25deg)
repeating-conic-gradient(#117 5deg, #ABE 15deg)

css5 0947

图 9-47. 重复的锥形渐变的三个变体

请注意,第一个(最左侧)示例中的平滑过渡即使在图像顶部也是成立的:从 350 度的#117到 5 度的#ABE的过渡与所有其他过渡处理方式相同。重复的锥形渐变在这种方式上是独特的,因为线性和径向渐变从不“环绕”,即末端不与起始点相接。这也可以在图 9-47 中的第三个(最右侧)示例中看到。

可以通过第二个(中心)示例来打破这种特殊行为:请注意从 355 度到 360 度的较窄楔形部分。这是因为图案中的第一个颜色停止明确地从 0 度到 5 度。因此,无法从 355 度过渡到 5 度,这导致在 360/0 度处出现了硬性过渡。

操控渐变图像

正如我们之前强调过的(可能有些过度),渐变是图像。这意味着您可以像处理任何 PNG 或 SVG 图像一样,使用各种背景属性对其进行大小、位置、重复和其他影响。

这可以通过重复简单的渐变来实现(更复杂的重复方法将在下一节讨论)。例如,您可以使用硬停止径向渐变来使背景呈现点状外观,如 图 9-48 所示:

body {background: radial-gradient(circle at center,
                    rgba(0 0 0 / 0.1), rgba(0 0 0 / 0.1) 10px,
                    transparent 10px, transparent)
                    center / 25px 25px repeat,
                    tan;}

css5 0948

图 9-48. 平铺径向渐变图像

是的,这在视觉上几乎与平铺具有直径为 10 像素的透明深色圆圈的 PNG 图像相同。在这种情况下使用渐变有三个优点:

  • CSS 文件的大小几乎肯定比相同 PNG 文件小。

  • 更重要的是,PNG 图像需要额外的服务器访问,这会减慢页面和服务器的性能。CSS 渐变是样式表的一部分,因此可以消除额外的服务器访问。

  • 更改渐变要简单得多,因此,通过实验找到确切的大小、形状和暗度变得更加容易。

创建特殊效果

渐变不能像光栅图像或矢量图像那样完成所有事情,因此并不意味着现在有了渐变就可以完全放弃外部图像。不过,您仍然可以通过渐变实现一些令人印象深刻的效果。考虑 图 9-49 中显示的背景效果。

css5 0949

图 9-49. 是时候播放音乐了…

那种帘子效果只需两个线性渐变,以不同的间隔重复,再加上第三个渐变在背景底部创建一个“发光”效果。以下是实现这一效果的代码:

background-image:
    linear-gradient(0deg, rgba(255 128 128 / 0.25), transparent 75%),
    linear-gradient(89deg,
        transparent 30%,
        #510A0E 35% 40%, #61100F 43%, #B93F3A 50%,
        #4B0408 55%, #6A0F18 60%, #651015 65%,
        #510A0E 70% 75%, rgba(255 128 128 / 0) 80%, transparent),
    linear-gradient(92deg,
        #510A0E 20%, #61100F 25%, #B93F3A 40%, #4B0408 50%,
        #6A0F18 70%, #651015 80%, #510A0E 90%);
background-size: auto, 300px 100%, 109px 100%;
background-repeat: repeat-x;

第一个(因此是顶部)渐变只是从 75% 透明的浅红色向上到渐变线的 75% 点处完全透明。然后创建两个“折叠”图像。图 9-50 分别显示了每个图像。

定义了这些图像后,它们沿 x 轴重复并给予不同的尺寸。第一个“发光”效果被赋予 auto 尺寸,以覆盖整个元素背景。第二个被赋予 300px 的宽度和 100% 的高度;因此,它将与元素背景一样高,宽度为 300 像素。这意味着它将沿 x 轴每 300 像素平铺一次。第三个图像也是如此,只是每 109 像素平铺一次。最终的效果看起来像是一个不规则的舞台幕布。

css5 0950

图 9-50. 两个“折叠”渐变

这种方法的美妙之处在于,调整平铺间隔只是编辑样式表的一件小事。如果您知道想要的效果,调整颜色停止位置或颜色就不那么复杂了。而且,如果您只需添加另一个渐变到堆栈中,添加第三组重复折叠也并不比较困难。

触发平均渐变颜色

值得问一下,如果重复渐变的第一个和最后一个颜色停止恰好处于同一位置会发生什么。例如,假设您的手指误按了 5 键,不小心声明如下内容:

repeating-radial-gradient(center, purple 0px, gold 0px)

第一个和最后一个颜色停止之间的距离为 0 像素,但渐变应该沿着渐变线无限重复。现在怎么办?

在这种情况下,浏览器找到平均渐变颜色,并在整个渐变图像中填充它。在我们先前代码中的简单情况下,这将是紫色和gold(大约是#C06C40rgb(75%,42%,25%))的 50/50 混合。因此,生成的渐变图像应该是一种纯橙褐色,看起来并不像是一个真正的渐变。

当浏览器将颜色停止位置四舍五入到 0 时,或者当第一个和最后一个颜色停止之间的距离与输出分辨率相比非常小而无法渲染任何有用的内容时,也可能会触发此条件。例如,如果重复的径向渐变使用了所有百分比的颜色停止位置,并且使用了closest-side来定义大小,但意外地放置在角落中,这种情况可能发生。

警告

截至 2022 年底,几乎没有浏览器能正确显示平均颜色。在非常有限的条件下可能会触发一些正确的行为,但在大多数情况下,浏览器要么只使用最后一个颜色停止作为填充颜色,要么试图非常努力地绘制亚像素重复的图案。

摘要

渐变是一种非常有趣的图像类型,完全由 CSS 值构成,而不是由光栅数据或矢量元素构成。有了三种可用的渐变类型,您几乎可以创建任何图案或视觉效果。

第十章:浮动和定位

长期以来,浮动元素是所有网页布局方案的基础。(这主要是因为clear属性,我们稍后会详细介绍。)但是浮动从未被用来进行布局;它们作为布局工具的使用几乎和使用表格进行布局一样糟糕。它们只是我们拥有的东西。然而,浮动元素本身非常有趣和有用。特别是考虑到最近添加的浮动形状,它允许创建内容可以流过的非矩形形状。

浮动

自从 20 世纪 90 年代初以来,通过声明,例如,<img src="b5.gif" alt="B4" align="right">,浮动图像就成为可能。这导致图像向右浮动,并允许其他内容(如文本)“环绕”图像。实际上,“浮动”这个名称来自 Netscape DevEdge 页面“HTML 2.0 扩展”,该页面解释了当时新的align属性。与 HTML 不同,CSS 允许您浮动任何元素,从图像到段落再到列表。这通过float属性实现。

例如,要将图像浮动到左侧,您可以使用以下标记:

<img src="b4.gif" style="float: left;" alt="b4">

如图 10-1 所示,图像“浮动”到浏览器窗口的左侧,并且文本环绕其周围。

css5 1001

图 10-1 浮动图像

您可以将元素浮动到leftright,也可以浮动到元素的inline-startinline-end边缘。当您希望将元素浮动到内联轴的起始或结束位置时,后两者非常有用,无论该轴的方向如何。(详见第六章关于内联轴的详细信息。)

注意

在这一节的其余部分中,我们主要使用leftright,因为它们简化了解释。至少在接下来的几年里,它们也几乎是唯一的float值。

浮动元素

在处理浮动元素时,请记住几点。首先,浮动元素在某种程度上脱离了文档的正常流,尽管它仍然影响正常流的布局。在 CSS 中,浮动元素几乎独立于其他文档,但它们仍然对文档的其余部分产生影响。

这种影响是因为当一个元素被浮动时,其他正常流内容会“环绕”它。这对于浮动图像来说是熟悉的行为,但是如果你浮动一个段落,例如,同样适用。在图 10-2 中,由于添加到浮动段落的边距,你可以清楚地看到这种效果:

p.aside {float: inline-end; width: 15em; margin: 0 1em 1em;
     padding: 0.25em; border: 1px solid;}

css5 1002

图 10-2 浮动段落

关于浮动元素的首要事实之一是,浮动元素周围的边距不会合并。如果您浮动一个图像并给它 25 像素的边距,那么该图像周围至少会有 25 像素的空间。如果与图像相邻的其他元素——这意味着水平垂直相邻——也具有边距,则这些边距不会与浮动图像上的边距合并。以下代码将生成图 10-3,两个浮动图像之间有 50 像素的间距:

p img {float: inline-start; margin: 25px;}

css5 1003

图 10-3。具有边距的浮动图像

完全不浮动

CSS 除了我们讨论过的值之外,还有一个float的其他值:float: none用于完全防止元素浮动。

这可能看起来有点傻,因为保持元素不浮动的最简单方法是避免声明float,对吧?嗯,首先,float的默认值是none。换句话说,为了使正常的非浮动行为成为可能,必须存在这个值;如果没有,所有元素都会以某种方式浮动。

其次,在某些情况下,您可能希望覆盖浮动。想象一下,您正在使用一个服务器范围内的样式表来浮动图像。在某个特定页面上,您不希望这些图像浮动。您可以在文档的嵌入样式表中添加img {float: none;},而不是编写全新的样式表。

浮动:细节

在我们深入浮动细节之前,建立包含块的概念非常重要。浮动元素的包含块是最近的块级祖先元素。因此,在以下标记中,浮动元素的包含块是包含它的段落元素:

<h1>
    Test
</h1>
<p>
    This is paragraph text, but you knew that. Within the content of this
    paragraph is an image that's been floated. <img src="testy.gif" alt=""
    class="floated-figure"> The containing block for the floated image is
    the paragraph.
</p>

当我们讨论“定位”时,我们将回到包含块的概念。

此外,浮动元素生成一个块级框,而不管它是何种类型的元素。因此,如果您浮动一个链接,即使该元素是行内的并且通常生成行内框,它也会生成一个块级框。它将被布局和操作,就好像它是一个<div>。这与为浮动元素声明display: block类似,尽管不必这样做。

在深入探讨应用行为之前,让我们先介绍一系列控制浮动元素位置的具体规则。这些规则与控制边距和宽度评估的规则有些相似,并且在初始外观上具有常识性。它们如下:

  1. 浮动元素的左(或右)外边缘不能位于其包含块的内边缘的左侧(或右侧)。

    这很简单。左浮动元素的外左边缘只能到达其包含块的内左边缘。同样,右浮动元素最远可以到达其包含块的内右边缘,如图 10-4 所示。(在本图及后续图中,带圈数字显示标记元素实际在源中的位置,带编号的框显示浮动可见元素的位置和大小。)

    css5 1004

    图 10-4. 向左(或向右)浮动
  2. 为防止与其他浮动元素重叠,浮动元素的左外边缘必须位于文档源中较早出现的左浮动元素的右外边缘右侧,除非后者的顶部低于前者的底部。同样,浮动元素的右外边缘必须位于文档源中较早出现的右浮动元素的左外边缘左侧,除非后者的顶部低于前者的底部。

    此规则防止浮动元素相互"覆盖"。如果一个元素向左浮动,并且已经有另一个浮动元素存在,后者将被放置在前一个浮动元素的外右边缘上。然而,如果浮动元素的顶部在所有较早浮动图像的底部以下,它可以一直浮动到父元素的内左边缘。图 10-5 展示了一些示例。

    css5 1005

    图 10-5. 防止浮动元素重叠

    这条规则的优点是,所有浮动内容都将可见,因为您不必担心一个浮动元素遮挡另一个浮动元素。这使得浮动变得相对安全。当使用定位时情况完全不同,定位很容易导致元素相互覆盖。

  3. 左浮动元素的右外边缘不得位于右浮动元素的左外边缘右侧。右浮动元素的左外边缘不得位于左浮动元素的右外边缘左侧。

    此规则防止浮动元素重叠。假设您有一个宽度为 500 像素的主体,并且其唯一内容是两个宽度为 300 像素的图像。第一个图像向左浮动,第二个向右浮动。该规则防止第二个图像重叠在第一个图像上,间距为 100 像素。相反,它被迫下移,直到其顶部位于右浮动图像的底部以下,如图 10-6 所示。

    css5 1006

    图 10-6. 更多重叠预防
  4. 浮动元素的顶部不得高于其父元素的内部顶部。如果浮动元素位于两个折叠边距之间,则将其放置为若有一个块级父元素位于这两个元素之间。

    此规则的第一部分防止浮动元素浮动到文档顶部。图 10-7 中展示了正确的行为。此规则的第二部分在某些情况下微调对齐方式,例如当三个段落中间有一个浮动段落时。在这种情况下,浮动段落会被视为有一个块级父元素(比如一个 <div>)。这可以防止浮动段落移动到这三个段落共享的任何公共父元素的顶部。

    css5 1007

    图 10-7. 与气球不同,浮动元素不能向上浮动
  5. 浮动元素的顶部不得高于先前任何浮动或块级元素的顶部。

    类似于规则 4,规则 5 防止浮动元素浮动到其父元素的顶部。浮动元素的顶部也不能高于先前出现的任何浮动元素的顶部。图 10-8 展示了一个例子:由于第二个浮动被迫在第一个浮动下方,第三个浮动的顶部与第二个浮动的顶部齐平,而不是第一个浮动的顶部。

    css5 1008

    图 10-8. 保持浮动元素低于其前置元素
  6. 浮动元素的顶部不得高于包含其先前生成的盒子的任何行框的顶部。

    类似于规则 4 和规则 5,此规则进一步限制元素的向上浮动,防止其位于包含先前内容的行框顶部之上。假设在段落中间有一张浮动的图片,那么该图片顶部可以放置的最高位置是其所在行框的顶部。正如您在图 10-9 中所见,这样可以防止图片向上浮动过高。

    css5 1009

    图 10-9. 保持浮动元素与其上下文的水平
  7. 如果左浮动元素左侧有另一个浮动元素,则其右外边缘不得位于其包含块的右边缘的右侧。同样,如果右浮动元素右侧有另一个浮动元素,则其右外边缘不得位于其包含块的左边缘的左侧。

    换句话说,浮动元素不能超出其包含元素的边缘,除非它太宽而无法自行容纳。这可以防止连续的浮动元素出现在水平行中并远超过包含块的边缘。相反,一个浮动元素如果会突出其包含块的边缘,例如图 10-10(在图中,浮动元素从下一行开始以更清晰地展示其工作原理)。

    css5 1010

    图 10-10. 如果没有足够的空间,浮动元素将被推到新的“行”上。
  8. 浮动元素必须尽可能高地放置。

    第 8 条规则如您所料,受前七条规则的限制。在历史上,浏览器将浮动元素的顶部与出现图像标签的行框顶部对齐。然而,第 8 条规则意味着其顶部应与其标签所在的同一行框的顶部对齐,假设有足够的空间。图 10-11 展示了理论上的正确行为。

    css5 1011

    图 10-11. 在其他限制条件的基础上,尽可能高地去
  9. 左浮动元素必须尽可能靠左放置,右浮动元素尽可能靠右。更高的位置优先于右侧或左侧更远的位置。

    同样,这条规则受前面规则的限制。正如您在图 10-12 中所看到的,很容易判断一个元素何时已尽可能向右或左移动。

    css5 1012

    图 10-12. 尽量靠左(或右)

应用行为

我们刚刚看到的规则产生了一些有趣的后果,因为它们说了什么,也因为它们没有说的。首先讨论的话题是当浮动元素比其父元素更高时会发生什么。

实际上,这种情况经常发生。例如,考虑一个由几个段落和 <h3> 元素组成的短文档,其中第一个段落包含一个浮动图像。此浮动图像的边距为 5 像素(5px)。您期望文档呈现如图 10-13 所示。

image

图 10-13. 预期的浮动行为

没有什么异常,但图 10-14 展示了当您将第一段设置为具有背景时会发生什么。

第二个示例与第一个示例没有任何不同,只是可见背景不同。如您所见,浮动图像伸出其父元素的底部。在第一个示例中也是如此,但那里不太明显,因为您看不到背景。我们之前讨论的浮动规则仅涉及浮动元素及其父元素的左、右和顶部边缘。有意忽略底部边缘需要在图 10-14 中看到的行为。

image

图 10-14. 背景和浮动元素

CSS 对此进行了澄清:浮动元素行为的一个重要方面是,浮动元素将扩展以包含任何浮动后代。因此,您可以通过浮动父元素来包含其父元素中的浮动元素,就像这个例子中一样:

<div style="float: left; width: 100%;">
    <img src="hay.gif" style="float: left;" alt=""> The 'div' will stretch
    around the floated image because the 'div' has been floated.
</div>

与此相关的是,考虑背景及其与文档中较早出现的浮动元素的关系,在图 10-15 中有所说明。

因为浮动元素既在流中又在流外,所以这种情况肯定会发生。到底发生了什么?标题的内容被浮动元素“移位”了。但是,标题的元素宽度仍然与其父元素一样宽。因此,其内容区域跨越父元素的宽度,背景也是如此。实际内容并不一直流过其自己的内容区域,以避免被浮动元素遮挡。

image

图 10-15. 元素背景“滑动”到浮动元素下

负边距

有趣的是,负边距可以导致浮动元素移动到其父元素之外。这似乎与前面解释的规则直接矛盾,但事实并非如此。通过负边距,元素可以看起来比其父元素更宽,浮动元素也可以看起来伸出其父元素之外。

让我们考虑一个图像,该图像向左浮动,并且左侧和顶部边距为-15px。此图像放置在一个没有填充、边框或边距的<div>中。图 10-16 显示了结果。

image

图 图 10-16. 负边距的浮动

与表面看上去的相反,这并不违反浮动元素被放置在其父元素之外的限制。

允许这种行为的技术细节在这里:仔细阅读前一节中的规则将表明,浮动元素的外边缘必须位于元素的父元素内。但是,负边距可以将浮动元素的内容放置在其自身的外边缘之外,详见图 10-17。

image

图 10-17. 使用负边距向上和向左浮动的详细信息

一个重要的问题是:当元素通过使用负边距从其父元素浮动出来时,文档显示会发生什么?例如,一个图像可能被浮动得如此之远,以至于它侵入了用户代理已经显示的段落中。在这种情况下,由用户代理决定是否应重新排列文档。

CSS 规范明确指出,用户代理不需要重新排列先前的内容以适应稍后在文档中发生的事情。换句话说,如果图像被浮动到前面的段落中,它可能会覆盖掉已经存在的任何内容。这使得浮动元素在负边距上的实用性有些受限。挂起的浮动通常是相当安全的,但试图将元素向上推在页面上通常是个坏主意。

另一种使浮动元素超出其父元素内部左右边缘的方式是,当浮动元素比其父元素更宽时。在这种情况下,浮动元素会溢出右侧或左侧内边缘——取决于元素的浮动方向——以最佳尝试正确显示自身。这会导致类似于图 10-18 所示的结果。

image

图 10-18. 浮动元素比其父元素更宽时的布局

浮动、内容和重叠

一个有趣的问题是:当浮动元素与正常流中的内容重叠时会发生什么?例如,如果浮动元素在内容流过的一侧具有负边距(例如,右浮动元素的负左边距),这种情况可能发生。您已经看到块级元素边框和背景的情况了,那么内联元素呢?

CSS 2.1 规范如下所述:

  • 与浮动元素重叠的内联框其边框、背景和内容都渲染在浮动元素的“上”。

  • 与浮动元素重叠的块级盒子其边框和背景渲染在浮动元素“后”,而其内容则渲染在浮动元素“上”。

为了说明这些规则,考虑以下情况:

<img src="testy.gif" alt="" class="sideline">
<p class="box">
    This paragraph, unremarkable in most ways, does contain an inline element.
    This inline contains some <strong>strongly emphasized text, which is so
    marked to make an important point</strong>. The rest of the element's
    content is normal anonymous inline content.
</p>
<p>
    This is a second paragraph.  There's nothing remarkable about it, really.
    Please move along to the next bit.
</p>
<h2 id="jump-up">
    A Heading!
</h2>

对该标记应用以下样式,并查看图 10-19 中的结果:

.sideline {float: left; margin: 10px -15px 10px 10px;}
p.box {border: 1px solid gray; background: hsl(117,50%,80%); padding: 0.5em;}
p.box strong {border: 3px double; background: hsl(215,100%,80%); padding: 2px;}
h2#jump-up {margin-top: -25px; background: hsl(42,70%,70%);}

image

图 10-19. 重叠浮动时的布局行为

内联元素(strong)完全覆盖了浮动图像——背景、边框、内容等。而块级元素仅其内容出现在浮动元素之上,背景和边框则位于浮动元素之后。

描述的重叠行为与文档源顺序无关。元素是在浮动元素之前还是之后出现都不重要:行为都是一样的。

清除

我们已经讨论了浮动行为,所以在我们转向形状之前,我们只需再讨论一个主题。您并不总是希望内容流过浮动元素——在某些情况下,您特别希望阻止它。如果您的文档被分组成部分,您可能不希望一个部分的浮动元素悬挂到下一个部分中。

在这种情况下,您会希望设置每个部分的第一个元素,以防止浮动元素出现在其旁边。如果第一个元素可能被放置在浮动元素旁边,它将被推到出现在浮动图像下方,并且所有后续内容将在其后显示,如 Figure 10-20 所示。

image

图 10-20. 显示一个清除元素

这是通过 clear 实现的。

例如,为了确保所有 <h3> 元素不会放置在左浮动元素的右侧,您可以声明 h3 {clear: left;}。这可以翻译为“确保 <h3> 的左侧清除浮动元素和伪元素”。以下规则使用 clear 阻止 <h3> 元素流过浮动元素到左侧:

h3 {clear: left;}

虽然这将把 <h3> 推过任何左浮动元素,但它将允许浮动元素出现在 <h3> 元素的右侧,如 Figure 10-21 所示。

image

图 10-21. 向左清除,但不向右清除

为了避免这种情况,并确保 <h3> 元素不与任何浮动元素在同一行上,您可以使用 both 值:

h3 {clear: both;}

理解这个值可以防止清除元素两侧与浮动元素并存,如示例 Figure 10-22 所示。

image

图 10-22. 两侧清除

另一方面,如果我们只担心 <h3> 元素被推到右侧浮动元素的下方,那么我们会使用 h3 {clear: right;}

float 类似,您可以为 clear 属性指定 inline-start(以及 both)或 inline-end 值。如果您使用这些值进行浮动,使用它们来清除是合理的。如果您使用 leftright 进行浮动,则使用这些值进行清除也是合理的。

最后,clear: none 允许元素浮动到元素的任一侧。与 float: none 类似,该值主要存在于允许正常文档行为的情况下,其中元素将允许浮动元素出现在两侧。none 值可用于覆盖其他样式,如 Figure 10-23 所示。尽管整个文档规定 <h3> 元素不允许浮动元素在任一侧出现,但特定的 <h3> 已设置为允许它在两侧出现:

h3 {clear: both;}

<h3 style="clear: none;">What's With All The NEO?</h3>

image

图 10-23. 一点都不清楚

clear属性通过清除的方式工作——在元素的顶部边距上方添加额外的间距,以将其推到任何浮动元素的上方。这意味着清除元素的顶部边距在清除元素时不会改变。它的向下移动是由清除引起的。请特别注意图 10-24 中标题边框的位置,这是由以下原因引起的:

img.sider {float: left; margin: 0;}
h3 {border: 1px solid gray; clear: left; margin-top: 15px;}

<img src="chrome.jpg" class="sider" height="50" width="50" alt="">
<img src="stripe.gif" height="10" width="100" alt="">
<h3>
    Why Doubt Salmon?
</h3>

![ 图片

图 10-24. 清除及其对边距的影响

<h3>的顶部边框与浮动图像的底部边框之间没有间隔,因为在上面增加了25 像素的间隙,以便将<h3>的顶部边框边缘推到浮动边缘的底部边缘之后。除非<h3>的顶部边距计算为40 像素或更多,否则会发生这种情况,此时<h3>将自然而然地放置在浮动下方,而clear值将无关紧要。

在大多数情况下,您无法知道元素需要清除多远。确保清除元素的顶部和浮动底部之间有一些空间的方法是在浮动本身上放置底部边距。因此,如果您希望在前面示例中的浮动下方至少有15 像素的空间,则会更改 CSS 如下:

img.sider {float: left; margin: 0 0 15px;}
h3 {border: 1px solid gray; clear: left;}

浮动元素的底部边距增加了浮动框的大小,因此清除元素必须被推动到的点。这是因为,正如以前所见,浮动元素的边距边缘定义了浮动框的边缘。

定位

定位背后的想法非常简单。它允许您精确地定义元素框相对于其原本应该出现的位置的位置,或者将其定位相对于父元素、另一个元素,甚至是视口(例如,浏览器窗口)本身。

在深入研究各种类型的定位之前,先看看存在哪些类型以及它们的区别是一个好主意。

定位类型

您可以通过使用position属性选择五种定位类型之一,这会影响元素框的生成方式。

position的值具有以下含义:

static

元素的框像往常一样生成。块级元素生成一个矩形框,该矩形框是文档流的一部分,而内联级别的框会导致在其父元素内流动一个或多个线框的创建。

relative

元素的框被偏移了一定的距离;默认为0px。元素保留了其如果未定位时将具有的形状,并且保留了元素本来会占用的空间。

absolute

元素的框完全从文档流中移除,并相对于其最近的定位祖先(如果有)或其包含块定位,这可能是文档中的另一个元素或初始包含块(在下一节中描述)。元素在正常文档流中可能占用的任何空间都被关闭,就好像元素不存在一样。定位元素生成块级框,而不管如果它在正常流中生成的框的类型。

fixed

元素的框表现得像被设为absolute一样,但其包含块是视口本身。

sticky

元素保持在正常流中,直到触发其粘性的条件满足为止,此时它将从正常流中移除,但其在正常流中的原始空间保留。然后它将像相对于其包含块绝对定位一样行事。一旦不再满足执行粘性的条件,元素将返回到其原始空间的正常流中。

现在不必过多担心细节,因为我们稍后将分别讨论这些种类的定位。在这之前,我们需要讨论包含块。

包含块

通常情况下,包含块 是包含另一个元素的框,正如我们在本章前面所说的那样。例如,在正常流的情况下,根元素(HTML 中的<html>)是<body>元素的包含块,后者依次是其所有子元素的包含块,依此类推。在定位时,包含块完全取决于定位的类型。

对于position值为relativestatic的非根元素,其包含块由最近的块级、表格单元格或内联块级祖先框的内容边缘形成。

对于具有position值为absolute的非根元素,其包含块被设定为最近的祖先(无论何种类型),其position值不是static。步骤如下:

  • 如果祖先是块级别的,则包含块被设定为该元素的填充边缘;换句话说,即边框将被包围的区域。

  • 如果祖先元素是内联级别的,包含块被设定为祖先元素的内容边缘。在从左到右的语言中,包含块的顶部和左侧是祖先元素中第一个框的顶部和左侧内容边缘,底部和右侧边缘是最后一个框的底部和右侧内容边缘。在从右到左的语言中,包含块的右边缘对应于第一个框的右内容边缘,左边缘取自最后一个框。顶部和底部保持不变。

  • 如果没有祖先,则元素的包含块被定义为初始包含块。

在处理粘性定位元素的包含块规则时,有一个有趣的变体,即矩形与包含块相关联的规则称为粘性约束矩形。这个矩形与粘性定位的工作原理有密切关系,并将在“粘性定位”中详细解释。

一个重要的观点:元素可以定位到其包含块之外。这表明术语“包含块”实际上应该是“定位上下文”,但由于规范使用“包含块”,因此我们也将使用它。

偏移属性

在前一节描述的四种定位方案中——相对定位、绝对定位、粘性定位和固定定位——使用不同的属性描述了定位元素边缘相对于其包含块的偏移量。这些属性被称为偏移属性,是定位工作的重要组成部分。有四个物理偏移属性和四个逻辑偏移属性。

这些属性描述了相对于包含块最近边缘的偏移量(因此称为偏移属性)。最简单的理解方法是,正值会造成内部偏移,将边缘移向包含块的中心,而负值则会造成外部偏移。

例如,top描述了定位元素的顶部边缘应距其包含块顶部的距离。在top的情况下,正值将使定位元素的顶部边缘向下移动,而负值将使其位于包含块顶部之上。类似地,left描述了定位元素的左边缘相对于包含块左边缘的偏移量。正值将使定位元素的边缘向右移动,而负值将使其向左移动。

偏移边距边缘的含义在于,可以为定位元素设置边距、边框和填充;这些将被保留并与定位元素一起保持,并且将包含在偏移属性定义的区域内。

重要的一点是,偏移属性定义的是相对于包含块的类似边缘的偏移量(例如,inset-block-end定义了相对于块结束边缘的偏移量),而不是相对于包含块的左上角。这就是为什么,例如,填满包含块的右下角的一种方法是使用这些值的原因:

top: 50%; bottom: 0; left: 50%; right: 0;

在这个例子中,定位元素的外左边缘位于包含块的一半处。这是它相对于包含块左边缘的偏移量。另一方面,定位元素的外右边缘没有偏移出包含块的右边缘,因此它们重合。对于定位元素的顶部和底部,类似的推理也成立:外顶边缘位于包含块的中间,但外底边缘并未从底部移动上来。这导致了 图 10-25 中所示的结果。

css5 1025

图 10-25. 填充包含块的右下角
注意

图 10-25 中所示的内容,以及本章大多数示例,都基于绝对定位。由于绝对定位是演示偏移属性如何工作的最简单方案,因此我们暂时保持这种方式。

注意定位元素的背景区域。在 图 10-25 中,它没有边距,但如果有的话,它们会在边框和偏移边缘之间创建空白空间。这会使得定位元素看起来好像没有完全填满包含块的右下角。事实上,它确实填充了这个区域,因为边距被视为定位元素区域的一部分,但这个事实可能不会立即显而易见。

因此,假设包含块的高度为 100em,宽度也为 100em,那么以下两组样式大致会产生相同的视觉效果:

#ex1 {top: 50%; bottom: 0; left: 50%; right: 0; margin: 10em;}
#ex2 {top: 60%; bottom: 10%; left: 60%; right: 10%; margin: 0;}

通过使用负的偏移值,我们可以将元素定位到其包含块之外。例如,以下数值将导致 图 10-26 所示的结果:

top: 50%; bottom: -2em; left: 75%; right: -7em;

css5 1026

图 10-26. 定位元素超出其包含块

除了长度和百分比值外,偏移属性还可以设置为 auto,这是默认值。auto 没有单一的行为;它会根据所使用的定位类型而改变。我们稍后将探讨 auto 的工作方式,逐个考虑每种定位类型。

内嵌简写

除了前面提到的逻辑内嵌属性外,CSS 还有几个内嵌的简写属性:两个逻辑的和一个物理的。

对于这两个属性,可以提供一个或两个值。如果提供一个值,则两侧使用相同的值;也就是说,inset-block: 10px 将在块起始边缘和块结束边缘都使用 10 像素的内嵌。

如果提供两个值,第一个值用于起始边缘,第二个值用于结束边缘。因此,inset-inline: 1em 2em 将在行内起始边缘使用 1 个 em 的内嵌,而在行内结束边缘使用 2 个 em 的内嵌。

使用逻辑偏移的这两个简写通常更容易,因为当你不想设置特定偏移时,可以始终提供auto,例如inset-block: 25% auto

所有四个边缘的简写属性称为inset,但它是一个物理属性——它是topbottomleftright的简写。

是的,看起来这应该是逻辑属性的简写,但它并不是。以下两个规则具有相同的结果:

#popup {top: 25%; right: 4em; bottom: 25%; left: 2em;}
#popup {inset: 25% 4em 25% 2em;}

与其他物理简写属性一样(如在第七章中所见),值的顺序为 TRBL(顶部、右侧、底部、左侧),省略的值将从相反的一侧复制。因此,inset: 20px 2em与编写inset: 20px 2em 20px 2em相同。

设置宽度和高度

确定了元素的位置后,通常会希望声明该元素的宽度和高度。此外,你可能还希望限制定位元素的高度或宽度。

如果你想为你的定位元素指定宽度,可以使用width属性。类似地,height将允许你为定位元素声明特定的高度。

尽管有时设置定位元素的widthheight很重要,但并不总是必要。例如,如果使用toprightbottomleft(或inset-block-startinset-inline-start等)描述了元素的四个边的放置方式,那么元素的heightwidth就会隐式地由偏移量决定。假设我们希望一个绝对定位的元素填充其包含块的左半部分,从顶部到底部。我们可以使用以下数值,其结果显示在图 10-27 中:

inset: 0 50% 0 0;

css5 1027

图 10-27. 仅使用偏移属性来定位和调整元素的大小

由于widthheight的默认值均为auto,因此显示在图 10-27 中的结果与我们使用以下数值完全相同:

inset: 0 50% 0 0; width: 50%; height: 100%;

在这个特定示例中,widthheight的存在对元素的布局没有任何影响。

如果我们要为元素添加填充、边框或外边距,则明确设置heightwidth的值可能会产生影响:

inset: 0 50% 0 0; width: 50%; height: 100%; padding: 2em;

这将使我们得到一个定位元素,其超出其包含块,如图 10-28 所示。

css5 1028

图 10-28. 部分超出其包含块的元素定位

这是因为(默认情况下)填充被添加到内容区域,而内容区域的大小由heightwidth的值确定。为了获得所需的填充并确保元素适合其包含块内,我们可以删除heightwidth声明,显式将它们都设置为auto,或将box-sizing设置为border-box

限制宽度和高度

如果有必要或者希望,可以通过以下属性对元素的宽度设置限制,我们称之为最小-最大属性。可以通过使用min-widthmin-height来定义元素的内容区域具有最小尺寸。

类似地,可以使用max-widthmax-height属性限制元素的尺寸。

这些属性的名称使它们相当易于理解。最初不太明显但经过思考后会变得合理的是,所有这些属性的值都不能为负值。

以下样式将强制定位元素的最小宽度为10em,高度为20em,如图 10-29 所示:

inset: 10% 10% 20% 50%; min-width: 10em; min-height: 20em;

image

图 10-29。为定位元素设置最小宽度和高度

这并不是一个非常健壮的解决方案,因为它强制元素至少具有一定大小,无论其包含块的大小如何。这里有一个更好的解决方案:

inset: 10% 10% auto 50%; height: auto; min-width: 15em;

在这里,元素的宽度应该是包含块宽度的 40%,但最小宽度不能少于15em。我们还修改了bottomheight,使它们自动确定。这样一来,元素高度可以根据需要调整,以显示其内容,无论宽度多窄(但绝不少于15em)。

注意

我们将在“绝对定位元素的放置和大小”中探讨auto在定位元素的高度和宽度中的作用。

您可以通过使用max-widthmax-height来避免元素过宽或过高。例如,假设出于某种原因,我们希望一个元素的宽度是其包含块宽度的四分之三,但在达到 400 像素时停止扩展。适当的样式如下:

width: 75%; max-width: 400px;

最小-最大属性的一个巨大优势是,它们允许您相对安全地混合单位。您可以在设置基于百分比的尺寸的同时设置基于长度的限制,反之亦然。

值得一提的是,这些最小-最大属性在与浮动元素结合使用时非常有用。例如,我们可以允许浮动元素的宽度相对于其父元素(即其包含块)的宽度而变化,同时确保浮动元素的宽度不会少于10em。反向方法也是可行的:

p.aside {float: left; width: 40em; max-width: 40%;}

这将使浮动元素宽度设置为40em,除非它超过包含块宽度的 40%,在这种情况下,浮动将被限制在 40%的宽度内。

注意

有关当内容溢出元素并被约束到特定最大大小时如何处理的详细信息,请参阅 “处理内容溢出”。

绝对定位

由于前几节中的大多数示例和图示都展示了绝对定位的应用,您已经看到了大量实际操作。剩下的内容大部分是在调用绝对定位时发生的详细情况。

包含块和绝对定位元素

当元素被绝对定位时,它完全从文档流中移除。然后,它相对于其最近的已定位祖先(如果有的话),否则相对于其包含块进行定位,并且其边距边缘使用偏移属性(topleftinset-inline-start等)进行放置。定位的元素不会围绕其他元素的内容流动,其他元素的内容也不会围绕定位的元素流动。这意味着绝对定位的元素可能会重叠其他元素,或者被其他元素重叠。(我们稍后会看到如何影响重叠顺序。)

绝对定位元素的包含块是最近的祖先元素,其 position 值不为 static。通常作者会选择一个元素作为绝对定位元素的包含块,并将其设置为 position: relative,没有偏移量,如下所示:

.contain {position: relative;}

考虑 图 10-30 中的示例,它说明了以下内容:

p {margin: 2em;}
p.contain {position: relative;} /* establish a containing block*/
b {position: absolute; inset: auto 0 0 auto;
    width: 8em; height: 5em; border: 1px solid gray;}
<body>
<p>
    This paragraph does <em>not</em> establish a containing block for any of
    its descendant elements that are absolutely positioned. Therefore, the
    absolutely positioned <b>boldface</b> element it contains will be
    positioned with respect to the initial containing block.
</p>
<p class="contain">
    Thanks to <code>position: relative</code>, this paragraph establishes a
    containing block for any of its descendant elements that are absolutely
    positioned. Since there is such an element-- <em>that is to say, <b>a
    boldfaced element that is absolutely positioned,</b> placed with respect
    to its containing block (the paragraph)</em>, it will appear within the
    element box generated by the paragraph.
</p>
</body>

两段落中的 <b> 元素都已经被绝对定位。区别在于每个元素使用的包含块不同。第一个段落中的 <b> 元素相对于初始包含块定位,因为其所有祖先元素的 position 都是 static。第二段落设置了 position: relative,因此为其后代元素建立了一个包含块。

image

图 10-30. 使用相对定位定义包含块

您可能已经注意到,在第二段落中,定位的元素重叠了段落的一些文本内容。除了将 <b> 元素定位到段落外部或为段落指定足够宽度的填充以容纳定位的元素外,没有其他方法可以避免这种情况。此外,由于 <b> 元素具有透明背景,段落的文本会透过定位的元素显示出来。唯一的避免方法是为定位的元素设置一个背景,或者将其完全移出段落。

假设包含块是根元素,您可以插入一个绝对定位的段落,如下所示,并获得与 图 10-31 中所示类似的结果:

<p style="position: absolute; top: 0; right: 25%; left: 25%; bottom:
 auto; width: 50%; height: auto; background: silver;">
    ...
</p>

现在,段落被定位在文档的开头,宽度为文档宽度的一半,并覆盖其他内容。

image

图 10-31。定位包含块为根元素的元素

需要强调的一个重要点是,当元素被绝对定位时,它为其后代元素建立一个包含块。例如,我们可以绝对定位一个元素,然后使用以下样式和基本标记绝对定位其子元素(在图 10-32 中描述):

div {position: relative; width: 100%; height: 10em;
    border: 1px solid; background: #EEE;}
div.a {position: absolute; top: 0; right: 0; width: 15em; height: 100%;
    margin-left: auto; background: #CCC;}
div.b {position: absolute; bottom: 0; left: 0; width: 10em; height: 50%;
    margin-top: auto; background: #AAA;}

<div>
    <div class="a">
        absolutely positioned element A
        <div class="b">
            absolutely positioned element B
        </div>
    </div>
    containing block
</div>

记住,如果文档有滚动,绝对定位元素也会随之滚动。这对所有不是固定位置或粘性位置元素的绝对定位元素都是适用的。

这是因为最终,这些元素是相对于正常流的某些部分定位的。例如,如果你绝对定位一个表格,而它的包含块是初始包含块,那么定位的表格将会滚动,因为初始包含块是正常流的一部分,因此会滚动。

如果你想要定位元素,使它们相对于视口定位并且不随文档的其余部分一起滚动,继续阅读。“固定定位”可以给出你所寻求的答案。

图片

图 10-32。绝对定位元素创建包含块

绝对定位元素的放置和大小

结合定位和大小的概念可能看起来很奇怪,但对于绝对定位元素来说,这是必需的,因为规范将它们紧密地绑定在一起。仔细思考一下,这并不是一个那么奇怪的组合。想象一下,如果一个元素是使用四个物理偏移属性定位的,会发生什么:

#masthead h1 {position: absolute; inset: 1em 25% 10px 1em;
    margin: 0; padding: 0; background: silver;}

在这里,<h1> 元素框的高度和宽度是由其外边距边缘的放置确定的,如图 10-33 所示。

图片

图 10-33。根据偏移属性确定元素的高度

如果包含块变得更高,<h1> 也会变得更高;如果包含块变窄,<h1> 也会变窄。如果我们给<h1>添加边距或填充,那会进一步影响其计算的高度和宽度。

但是如果我们已经做了这一切,然后还尝试设置显式的高度和宽度呢?

#masthead h1 {position: absolute; top: 0; left: 1em; right: 10%; bottom: 0;
    margin: 0; padding: 0; height: 1em; width: 50%; background: silver;}

有些地方必须让步,因为所有这些值都几乎不可能都准确。事实上,包含块的宽度必须恰好是<h1> 计算出的font-size 的两倍半,才能使所有显示的值都准确。任何其他的width 都意味着至少有一个值是错误的,必须被忽略。确定哪个值取决于多个因素,并且这些因素根据元素是替换还是非替换而改变。 (见第六章 替换与非替换元素。)

此外,考虑以下情况:

#masthead h1 {position: absolute; top: auto; left: auto;}

结果应该是什么?事实上,答案并不是“将值重置为 0”。我们将在下一节中看到真正的答案。

自动边缘

当绝对定位一个元素时,当偏移属性(除bottom以外的任何属性)设置为auto时,会应用特殊的行为。以top为例。考虑以下情况:

<p>
    When we consider the effect of positioning, it quickly becomes clear that
    authors can do a great deal of damage to layout, just as they can do very
    interesting things.<span style="position: absolute; top: auto;
 left: 0;">[4]</span> This is usually the case with useful technologies:
    the sword always has at least two edges, both of them sharp.
</p>

发生了什么?对于left,元素的左边缘应该与其包含块的左边缘对齐(我们在此假设为初始包含块)。

然而,对于top来说,会发生更有趣的事情。定位元素的顶部应与其如果根本未定位时其顶部将要放置的位置对齐。换句话说,想象一下如果其position值为static<span>会被放置的地方;这就是其静态位置——应该计算其顶部边缘放置的位置。因此,我们应该得到图 10-34 中显示的结果。

image

图 10-34。将元素绝对定位于其“静态”顶部边缘

“[4]”位于段落内容的外部,因为初始包含块的左边缘在段落的左边缘左侧。

对于将leftright设置为auto,相同的基本规则适用。在这些情况下,定位元素的左(或右)边缘与如果元素未定位时边缘将要放置的位置对齐。因此,让我们修改前面的例子,使topleft都设置为auto

<p>
    When we consider the effect of positioning, it quickly becomes clear that
    authors can do a great deal of damage to layout, just as they can do very
    interesting things.<span style="position: absolute; top: auto; left:
 auto;">[4]</span> This is usually the case with useful technologies:
    the sword always has at least two edges, both of them sharp.
</p>

结果见图 10-35。

image

图 10-35。将元素绝对定位于其“静态”位置

现在,“[4]”位于其未定位时应该放置的位置。请注意,由于它定位,其正常流空间被关闭。这导致定位元素重叠正常流内容。

此自动放置仅在某些情况下有效,通常是在定位元素的其他尺寸维度上没有约束的情况下。我们之前的例子可以自动放置,因为它对其高度或宽度没有约束,也没有对底部和右侧边缘放置的约束。但假设,出于某种原因,确实有这样的约束。考虑以下情况:

<p>
    When we consider the effect of positioning, it quickly becomes clear that
    authors can do a great deal of damage to layout, just as they can do very
    interesting things.<span style="position: absolute; inset: auto 0 0 auto;
 height: 2em; width: 5em;">[4]</span> This is usually the case with useful
    technologies: the sword always has at least two edges, both of them sharp.
</p>

不可能满足所有这些值。确定发生的事情是下一节的主题。

放置和调整非替换元素

通常,元素的大小和位置取决于其包含块。其各种属性(widthrightpadding-left等)的值影响其布局,但基础是包含块。

考虑定位元素的宽度和水平位置。可以表示为以下方程式:

left + margin-left + border-left-width + padding-left + width +
padding-right + border-right-width + margin-right + right =
the width of the containing block

这个计算是相当合理的。它基本上是确定正常流中块级元素大小的方程,只是增加了leftright。那么所有这些如何交互?我们有一系列规则可供参考。

首先,如果leftwidthright都设置为auto,则会得到前一节中所见的结果:左边缘被放置在其静态位置,假设是左到右的语言。在右到左的语言中,右边缘被放置在其静态位置。元素的宽度被设置为“收缩适应”,这意味着元素的内容区域仅宽到足以容纳其内容。非静态位置属性(左到右语言中为right,右到左语言中为left)被设置为占据剩余的距离。例如:

<div style="position: relative; width: 25em; border: 1px dotted;">
    An absolutely positioned element can have its content <span style="position:
 absolute; top: 0; left: 0; right: auto; width: auto; background:
 silver;">shrink-wrapped</span> thanks to the way positioning rules work.
</div>

这导致了图 10-36。

image

图 10-36. 绝对定位元素的“收缩适应”行为

元素的顶部放置在其包含块的顶部(在本例中为<div>),元素的宽度正好足够容纳内容。从元素右边缘到包含块右边缘的剩余距离成为right的计算值。

现在假设只有左右边距被设置为auto,而不是leftwidthright,如此示例:

<div style="position: relative; width: 25em; border: 1px dotted;">
    An absolutely positioned element can have its content <span style="position:
 absolute; top: 0; left: 1em; right: 1em; width: 10em; margin: 0 auto;
 background: silver;">shrink-wrapped</span> thanks to the way positioning
    rules work.
</div>

这里发生的是左右边距都设置为auto,并且相等。这将有效地使元素居中,如图 10-37 所示。

image

图 10-37. 使用auto边距水平居中绝对定位元素

这基本上与正常流中的auto边距居中相同。因此,让我们将边距设置为除了auto之外的其他值:

<div style="position: relative; width: 25em; border: 1px dotted;">
    An absolutely positioned element can have its content <span style="position:
 absolute; top: 0; left: 1em; right: 1em; width: 10em; margin-left: 1em;
 margin-right: 1em; background: silver;">shrink-wrapped</span> thanks to the
    way positioning rules work.
</div>

现在我们有一个问题。定位的<span>属性总和仅为14em,而包含块的宽度为25em。这意味着我们需要弥补 11 个 em 的差额。

规则说明,在这种情况下,用户代理忽略元素内联末端的值,并为其求解。换句话说,结果将与我们声明的结果相同:

<span style="position: absolute; top: 0; left: 1em;
right: 12em; width: 10em; margin-left: 1em; margin-right: 1em;
right: auto; background: silver;">shrink-wrapped</span>

这导致了图 10-38。

image

图 10-38. 在超约束情况下忽略right的值

如果其中一个边距被设置为auto,那将会发生变化。假设我们改变样式如下所述:

<span style="position: absolute; top: 0; left: 1em;
right: 1em; width: 10em; margin-left: 1em; margin-right: auto;
background: silver;">shrink-wrapped</span>

视觉结果将与图 10-38 相同,只是通过计算右边距为12em来达到,而不是覆盖给属性right分配的值。

另一方面,如果我们将左边距设为auto将被重置,如图 10-39 所示:

<span style="position: absolute; top: 0; left: 1em;
right: 1em; width: 10em; margin-left: auto; margin-right: 1em;
background: silver;">shrink-wrapped</span>

image

图 10-39. 利用 auto 左边距

通常情况下,如果只有一个属性设置为 auto,那么该属性将用于满足前面章节中给出的方程。因此,根据以下样式,元素的宽度会扩展到需要的任何尺寸,而不是“包裹”内容:

<span style="position: absolute; top: 0; left: 1em;
right: 1em; width: auto; margin-left: 1em; margin-right: 1em;
background: silver;">not shrink-wrapped</span>

到目前为止,我们主要考察了沿水平轴的行为,但沿垂直轴也适用非常相似的规则。如果我们将之前的讨论旋转 90 度,实际上我们得到的几乎是相同的行为。例如,以下标记会导致图 10-40 的结果:

<div style="position: relative; width: 30em; height: 10em; border: 1px solid;">
    <div style="position: absolute; left: 0; width: 30%;
 background: #CCC; top: 0;">
            element A
    </div>
    <div style="position: absolute; left: 35%; width: 30%;
 background: #AAA; top: 0; height: 50%;">
            element B
    </div>
    <div style="position: absolute; left: 70%; width: 30%;
 background: #CCC; height: 50%; bottom: 0;">
            element C
    </div>
</div>

在第一种情况下,元素的高度会被包裹到内容。在第二种情况下,未指定的属性 (bottom) 被设置为弥补定位元素底部与其包含块底部之间的距离。在第三种情况下,top 未指定,因此用于弥补差距。

图片

图 10-40. 绝对定位元素的垂直布局行为

就这一点而言,自动边距可以导致垂直居中。根据以下样式,绝对定位的 <div> 将在其包含块内垂直居中,如图 10-41 所示:

<div style="position: relative; width: 10em; height: 10em; border: 1px solid;">
    <div style="position: absolute; left: 0; width: 100%; background: #CCC;
 top: 0; height: 5em; bottom: 0; margin: auto 0;">
            element D
    </div>
</div>

css5 1041

图 10-41. 使用自动边距垂直居中绝对定位元素

还有两个小变化需要指出。在水平布局中,如果值为 auto,则 rightleft 可以根据静态位置放置。在垂直布局中,只有 top 可以采用静态位置;由于某种原因,bottom 不能。

此外,如果绝对定位元素在垂直方向上尺寸过多约束,bottom 将被忽略。因此,在以下情况下,bottom 的声明值将被计算值 5em 覆盖:

<div style="position: relative; width: 10em; height: 10em; border: 1px solid;">
    <div style="position: absolute; left: 0; width: 100%; background: #CCC;
 top: 0; height: 5em; bottom: 0; margin: 0;">
            element D
    </div>
</div>

如果属性过多,top 无法被忽略。

放置和调整替换元素

替换元素(例如图像)的定位规则与非替换元素不同。这是因为替换元素具有固有的高度和宽度,因此除非作者明确更改,否则不会改变。因此,在替换元素的定位中没有“收缩以适应”概念。

放置和调整替换元素的行为可以通过以下一系列规则最容易地表达,逐一采取:

  1. 如果 width 设置为 auto,则 width 的使用值由元素内容的固有宽度确定。因此,如果图像固有宽度为 50 像素,则使用值计算为 50px。如果显式声明了 width(例如 100px50%),则宽度设置为该值。

  2. 如果 left 在从左到右的语言环境中具有 auto 值,则将其替换为静态位置。在从右到左的语言环境中,将 right 上的 auto 值替换为静态位置。

  3. 如果 leftright 仍然是 auto(换句话说,在之前的步骤中它还没有被替换),则将 margin-leftmargin-right 上的任何 auto 替换为 0

  4. 如果此时 margin-leftmargin-right 都仍然被定义为 auto,则将它们设置为相等,从而使元素在其包含块中居中。

  5. 最后,如果只剩下一个 auto 值,将其更改为等于方程的余数。

这导致与绝对定位的非替换元素看到的相同基本行为,只要你假设非替换元素有一个显式的 width。因此,以下两个元素将具有相同的宽度和位置,假设图像的固有宽度为 100 像素(参见 图 10-42):

<div>
    <img src="frown.gif" alt="a frowny face"
        style="position: absolute; top: 0; left: 50px; margin: 0;">
</div>
<div style="position: absolute; top: 0; left: 50px;
 width: 100px; height: 100px; margin: 0;">
    it's a div!
</div>

css5 1042

图 10-42. 绝对定位替换元素

与非替换元素类似,如果值被过度约束,用户代理应忽略行内结束端上的值:在从左到右的语言中为 right,在从右到左的语言中为 left。因此,在以下示例中,right 的声明值被计算为 50px 覆盖了:

<div style="position: relative; width: 300px;">
    <img src="frown.gif" alt="a frowny face" style="position: absolute; top: 0;
 left: 50px; right: 125px; width: 200px; margin: 0;">
</div>

类似地,沿垂直轴的布局受以下一系列规则的控制:

  1. 如果 height 设置为 auto,则 height 的计算值由元素内容的固有高度决定。因此,高度为 50 像素的图像的计算值为 50px。如果显式声明了 height(比如 100px50%),则高度将设置为该值。

  2. 如果 top 的值为 auto,则用替换元素的静态位置替换它。

  3. 如果 bottom 的值为 auto,则将 margin-topmargin-bottom 上的任何 auto 值替换为 0

  4. 如果此时 margin-topmargin-bottom 都仍然被定义为 auto,则将它们设置为相等,从而使元素在其包含块中居中。

  5. 最后,如果只剩下一个 auto 值,将其更改为等于方程的余数。

与非替换元素类似,如果值被过度约束,用户代理应忽略 bottom 的值。

因此,以下标记结果为 图 10-43:

<div style="position: relative; height: 200px; width: 200px; border: 1px solid;">
    <img src="one.gif" alt="one" width="25" height="25"
        style="position: absolute; top: 0; left: 0; margin: 0;">
    <img src="two.gif" alt="two" width="25" height="25"
        style="position: absolute; top: 0; left: 60px; margin: 10px 0;
 bottom: 4377px;">
    <img src="three.gif" alt="three" width="25" height="25"
        style="position: absolute; left: 0; width: 100px; margin: 10px;
 bottom: 0;">
    <img src="four.gif" alt="four" width="25" height="25"
        style="position: absolute; top: 0; height: 100px; right: 0;
 width: 50px;">
    <img src="five.gif" alt="five" width="25" height="25"
        style="position: absolute; top: 0; left: 0; bottom: 0; right: 0;
 margin: auto;">
</div>

css5 1043

图 10-43. 通过定位拉伸替换元素

Z 轴上的放置

随着所有定位的进行,不可避免地会出现两个元素试图在视觉上占据相同位置的情况。其中一个将会覆盖另一个,那么我们如何控制哪个元素显示在“顶部”呢?这就是 z-index 发挥作用的地方。

这一属性允许您改变元素重叠的方式。它的名字源自坐标系,其中从左到右是 x 轴,从上到下是 y 轴。在这种情况下,从背后到前面的第三轴被称为z 轴。因此,通过使用z-index,元素沿这个轴被赋予值。图 10-44 展示了这个系统。

css5 1044

图 10-44. z-index 堆叠的概念视图

在这个坐标系中,具有较高z-index值的元素比具有较低z-index值的元素更接近读者。这将导致高值元素重叠其他元素,如图 10-45 所示,这是图 10-44 的“头对头”视图。这种重叠优先级称为堆叠

image

图 10-45. 元素的堆叠方式

任何整数都可以作为z-index的值,包括负数。给一个元素赋予负的z-index值会将其移动到离读者更远的位置;也就是说,它会在堆叠中放置得更低。考虑以下样式,如图 10-46 所示:

p {background: rgba(255,255,255,0.9); border: 1px solid;}
p#first {position: absolute; top: 0; left: 0;
    width: 40%; height: 10em; z-index: 8;}
p#second {position: absolute; top: -0.75em; left: 15%;
    width: 60%; height: 5.5em; z-index: 4;}
p#third {position: absolute; top: 23%; left: 25%;
    width: 30%; height: 10em; z-index: 1;}
p#fourth {position: absolute; top: 10%; left: 10%;
    width: 80%; height: 10em; z-index: 0;}

每个元素根据其样式进行定位,但堆叠的常规顺序通过z-index值进行了改变。假设段落按数字顺序排列,合理的堆叠顺序应该是,从低到高,p#firstp#secondp#thirdp#fourth。这会将p#first置于其他三个元素后面,而p#fourth置于其他元素前面。由于z-index,堆叠顺序在您控制之下。

image

图 10-46. 堆叠元素可以重叠

如前面的例子所示,z-index值不需要连续。您可以分配任何大小的任何整数。如果您希望某个元素保持在其他所有元素的前面,您可以使用类似于z-index: 100000的规则。在大多数情况下,这将按预期工作 —— 尽管如果您声明另一个元素的z-index100001(或更高),它将显示在最前面。

一旦为元素分配了z-index值(除了auto之外),该元素将建立自己的本地堆叠上下文。这意味着所有该元素的后代元素有它们自己的堆叠顺序,但相对于它们的祖先元素而言。这与元素建立新的包含块的方式非常相似。根据以下样式,您将看到类似于图 10-47 的效果:

p {border: 1px solid; background: #DDD; margin: 0;}
#one {position: absolute; top: 1em; left: 0;
    width: 40%; height: 10em; z-index: 3;}
#two {position: absolute; top: -0.75em; left: 15%;
    width: 60%; height: 5.5em; z-index: 10;}
#three {position: absolute; top: 10%; left: 30%;
    width: 30%; height: 10em; z-index: 8;}
p[id] em {position: absolute; top: -1em; left: -1em;
    width: 10em; height: 5em;}
#one em {z-index: 100; background: hsla(0,50%,70%,0.9);}
#two em {z-index: 10; background: hsla(120,50%,70%,0.9);}
#three em {z-index: -343; background: hsla(240,50%,70%,0.9);}

image

图 10-47. 定位元素建立本地堆叠上下文

请注意 <em> 元素在堆叠顺序中的位置(您可以在 “孤立混合” 中找到建立堆叠上下文的各种方法的列表,在 第二十章 中)。每个元素都与其父元素正确分层。每个 <em> 都位于其父元素的前面,无论其 z-index 是否为负,并且父元素和子元素被像编辑程序中的图层一样分组在一起。 (规范在使用 z-index 堆叠时阻止子元素被绘制在其父元素后面,因此 p#three 中的 em 被绘制在 p#one 的顶部,即使其 z-index 值为 -343。)这是因为其 z-index 值是相对于其局部堆叠上下文的取值:其包含块。而该包含块反过来具有一个 z-index,它在其局部堆叠上下文中起作用。

我们还有一个 z-index 值需要检查。CSS 规范对默认值 auto 有以下说明:

生成的盒子在当前堆叠上下文中的堆叠级别为 0. 该盒子不会创建新的堆叠上下文,除非它是根元素。

因此,任何具有 z-index: auto 的元素可以被视为设置为 z-index: 0

小贴士

即使不使用 position 属性定位,z-index 也会被 flex 和 grid 项目所尊重。规则本质上是相同的。

固定定位

如前一节所示,固定定位 就像绝对定位一样,唯一不同的是固定元素的包含块是 视口。固定定位的元素完全从文档流中移除,并且不相对于文档的任何部分定位。

固定定位可以以有趣的方式进行利用。首先,可以通过使用固定定位创建类似框架的界面。考虑 图 10-48,显示了一个常见的布局方案。

image

图 10-48. 使用固定定位模拟框架

可以使用以下样式完成这个过程:

header {position: fixed; top: 0; bottom: 80%; left: 20%; right: 0;
    background: gray;}
div#sidebar {position: fixed; top: 0; bottom: 0; left: 0; right: 80%;
    background: silver;}

这将使页眉和侧边栏固定在视口的顶部和侧边,无论文档如何滚动,它们都会保持在那里。然而,这里的缺点是文档的其余内容会被固定元素覆盖。因此,其余内容可能应该包含在自己的包装元素中,并使用以下类似方法:

main {position: absolute; top: 20%; bottom: 0; left: 20%; right: 0;
    overflow: scroll; background: white;}

通过添加适当的边距,甚至可以在三个定位元素之间创建小间隙,如下所示:

body {background: black; color: silver;} /* colors for safety's sake */
div#header {position: fixed; top: 0; bottom: 80%; left: 20%; right: 0;
    background: gray; margin-bottom: 2px; color: yellow;}
div#sidebar {position: fixed; top: 0; bottom: 0; left: 0; right: 80%;
    background: silver; margin-right: 2px; color: maroon;}
div#main {position: absolute; top: 20%; bottom: 0; left: 20%; right: 0;
    overflow: auto; background: white; color: black;}

鉴于这种情况,可以将平铺图像应用于 <body> 的背景。如果作者认为合适,可以通过边距创建的间隙显示这个图像。

固定定位的另一个用途是将“持久”元素放置在屏幕上,例如一小段链接列表。我们可以像以下这样创建一个持久的 footer,包含版权和其他信息:

footer {position: fixed; bottom: 0; width: 100%; height: auto;}

这会将footer元素放置在视口底部,并且无论文档滚动多少,都会保持在那里。

注意

固定定位的许多布局案例,除了“持久元素”外,都可以通过网格布局(详见第十二章)处理得更好。

相对定位

最简单的定位方案是相对定位。在这种方案中,定位元素通过偏移属性进行移动。然而,这可能会带来一些有趣的后果。

表面上看,这似乎很简单。假设我们想要将一个图像向上和向左移动。这些样式的结果如图 10-49 所示:

img {position: relative; top: -20px; left: -20px;}

image

图 10-49. 一个相对定位元素

我们在这里所做的只是将图像的顶边向上偏移了 20 像素,并将左边缘向左偏移了 20 像素。然而,请注意,图像本应存在的空白空间没有消失。这是因为当一个元素相对定位时,它会从其正常位置偏移,但它原本占据的空间并没有消失。

注意

相对定位与翻译元素转换非常相似,这在第十七章中有所讨论。

考虑以下样式的结果,这些样式如图 10-50 所示:

em {position: relative; top: 10em; color: red;}

image

图 10-50. 另一个相对定位元素

如您所见,段落中有些空白空间。这是<em>元素原本的位置,而<em>元素在其新位置的布局正好与它留下的空间完全相同。

相对定位元素也可以移动到重叠其他内容上。例如,以下样式和标记在图 10-51 中有详细说明:

img.slide {position: relative; left: 30px;}
<p>
    In this paragraph, we will find that there is an image that has been
    pushed to the right. It will therefore <img src="star.gif" alt="A star!"
    class="slide"> overlap content nearby, assuming that it is not the
    last element in its line box.
</p>

image

图 10-51. 相对定位元素可以重叠其他内容

相对定位有一个有趣的特点。当一个相对定位的元素过度约束时会发生什么?例如:

strong {position: relative; top: 10px; bottom: 20px;}

在这里,我们有两个调用非常不同行为的值。如果仅考虑top: 10px,元素应向下移动 10 像素,但是bottom: 20px显然要求元素向上移动 20 像素。

CSS 规定,当涉及过度约束的相对定位时,一个值会被重置为另一个的负值。因此,bottom始终等于-top。这意味着前面的示例将被视为以下内容:

strong {position: relative; top: 10px; bottom: -10px;}

因此,<strong>元素将向下偏移 10 像素。规范也允许书写方向。在相对定位中,right始终等于左边语言中的-left;但在右至左语言中,情况则相反:left始终等于-right

注意

正如您在前面的章节中看到的那样,当我们相对定位一个元素时,它立即为其子元素建立一个新的包含块。这个包含块对应于元素被新定位的位置。

粘性定位

CSS 中的最后一种定位类型是粘性定位。如果您曾经在移动设备上使用过一个不错的音乐应用程序,您可能已经注意到这种效果:当您滚动通过按字母排序的艺术家列表时,当前字母会固定在窗口顶部,直到进入新的字母部分时,新的字母会替换旧的。在打印中很难显示,但图 10-52 试图通过显示滚动中的三个点来解释它。

image

图 10-52。粘性定位

CSS 通过声明元素为position: sticky,使得这种效果成为可能,但通常情况下,这还不够。

首先,偏移量(topleft等)用于相对于包含块定义粘性定位矩形。以以下内容为例。它将产生图 10-53 中所示的效果,其中虚线显示了粘性定位矩形的创建位置:

#scrollbox {overflow: scroll; width: 15em; height: 18em;}
#scrollbox h2 {position: sticky; top: 2em; bottom: auto;
    left: auto; right: auto;}

css5 1053

图 10-53。粘性定位矩形

请注意,<h2>位于图 10-53 中的矩形中间。这是它在#scrollbox元素内部内容正常流中的位置。要使<h2>粘性,唯一的方法是滚动内容,直到<h2>的顶部触及粘性定位矩形的顶部(即位于 scrollbox 顶部以下2em的位置)—在此之后,<h2>将会粘在那里。这在图 10-54 中有所说明。

image

图 10-54。粘在粘性定位矩形顶部

换句话说,<h2>在正常流中,直到其粘性边缘触及粘性定位矩形的粘性边缘。在那一点上,它会像绝对定位一样粘在那里,不过会在正常流中留下本应占据的空间。

你可能已经注意到,#scrollbox元素没有position声明。它也没有被隐藏到幕后:正是#scrollbox上设置的overflow: scroll创建了粘性定位的<h2>元素的包含块。这是一个情况,其中包含块并非由position决定。

如果滚动反向,使得<h2>的正常流位置低于矩形的顶部,<h2>将从矩形中分离,并恢复到正常流中的位置。这在图 10-55 中有所显示。

image

图 10-55。从粘性定位矩形顶部脱离

请注意,在这些示例中,<h2> 粘在矩形的顶部是因为 <h2>top 值设置为非 auto(即粘性定位元素)。你可以使用任何你想要的偏移侧面。例如,你可以让元素在向下滚动内容时粘在矩形的底部。以下代码在 图 10-56 中有所体现:

#scrollbox {overflow: scroll; position: relative; width: 15em; height: 10em;}
#scrollbox h2 {position: sticky; top: auto; bottom: 0; left: auto; right: auto;}

image

图 10-56. 粘在粘性定位矩形的底部

例如,这可以是显示给定段落的脚注或评论的一种方式,同时允许它们随着段落向上移动而滚动消失。同样的规则也适用于左右两侧,这对于侧向滚动内容非常有用。

如果你定义了多个偏移属性,并且它们的值不是 auto,那么所有这些属性都将成为粘性边缘。例如,这组样式将强制 <h2> 始终出现在滚动框内,无论其内容如何滚动(见 图 10-57):

#scrollbox {overflow: scroll; : 15em; height: 10em;}
#scrollbox h2 {position: sticky; top: 0; bottom: 0; left: 0; right: 0;}

image

图 10-57. 使每个边都成为粘性边

你可能会想:如果我在这种情况下有多个粘性定位的元素,并且我滚动超过两个或更多,会发生什么?事实上,它们会相互堆叠在一起:

#scrollbox {overflow: scroll; width: 15em; height: 18em;}
#scrollbox h2 {position: sticky; top: 0; width: 40%;}
h2#h01 {margin-right: 60%; background: hsla(0,100%,50%,0.75);}
h2#h02 {margin-left: 60%; background: hsla(120,100%,50%,0.75);}
h2#h03 {margin-left: auto; margin-right: auto;
    background: hsla(240,100%,50%,0.75);}

在静态图像中(例如 图 10-58),很难看出标题堆叠的方式,但它们在源文件中的后面,它们离观察者越近。这是常见的 z-index 行为——这意味着您可以通过指定显式的 z-index 值来决定哪些粘性元素位于其他元素之上。例如,假设我们希望内容中的第一个粘性元素位于所有其他元素之上。通过给它 z-index: 1000 或任何足够高的数值,它将位于所有其他粘性元素的顶部。视觉效果将是其他元素“滑动在”最顶部元素下方。

image

图 10-58. 一个粘性头部的堆积

摘要

正如本章中所示,CSS 提供了多种影响基本元素位置的方法。浮动可能是 CSS 的一个基本简单方面,但这并不妨碍它们的有用和强大。它们填补了一个重要且光荣的空白,允许内容一边靠在一边的一侧。

由于定位的存在,我们可以以正常流无法实现的方式移动元素。结合 z 轴的堆叠可能性和各种溢出模式,即使在我们可以使用 Flexbox 和 Grid 布局的时代,定位仍然有很多优点。

第十一章:伸缩盒布局

CSS 伸缩盒模块 1 级,或简称伸缩盒,使得一度困难的页面、小部件、应用程序和画廊布局几乎变得简单。通过伸缩盒,通常不需要 CSS 框架。在本章中,您将学习如何只用几行 CSS 创建站点所需的几乎任何功能。

伸缩盒基础知识

伸缩盒 是一种简单而强大的页面组件布局方式,通过定义空间分配方式、内容对齐和元素的视觉顺序来实现。内容可以轻松垂直或水平排列,并可以沿单一轴或跨多行进行布局。还有更多更多。

使用伸缩盒,内容的外观可以独立于源代码顺序。虽然视觉上会改变,但伸缩属性不应影响屏幕阅读器读取内容的顺序。

警告

规范说明屏幕阅读器应该遵循源代码顺序,但截至 2022 年底,Firefox 遵循视觉顺序。目前,有一个提案建议添加一个 CSS 属性,指定是遵循源代码还是视觉顺序,因此很快可能可以自行决定。

或许更重要的是,采用灵活盒模块布局后,元素可以在不同屏幕大小和不同显示设备上表现出可预测的行为。伸缩盒在响应式站点中非常有效,因为在提供的空间增加或减少时,内容可以增加或减小尺寸。

伸缩盒通过声明 display: flexdisplay: inline-flex 来激活父子关系布局。这个元素成为伸缩容器,在提供的空间内排列其子元素并控制它们的布局。这个伸缩容器的子元素成为伸缩项目。考虑以下样式和标记,见 图 11-1:

div#one {display: flex;}
div#two {display: inline-flex;}
div {border: 1px dashed; background: silver;}
div > * {border: 1px solid; background: #AAA;}
div p {margin: 0;}
<div id="one">
    <p>flex item with<br>two longer lines</p>
    <span>flex item</span>
    <p>flex item</p>
</div>
<div id="two">
    <span>flex item with<br>two longer lines</span>
    <span>flex item</span>
    <p>flex item</p>
</div>

添加  或  创建伸缩容器

图 11-1. 两种伸缩容器类型
提示

查找播放符号 以了解在线示例是否可用。本章中的所有示例都可以在 https://meyerweb.github.io/csstdg5figs/11-flexbox 找到。

注意 <div> 的每个子元素如何成为伸缩项目,以及它们如何以相同方式布局?这些元素不同于某些段落和其他 <span>,它们都成为伸缩项目。(除了浏览器默认边距,可能会有一些差异,但已经移除。)

第一个和第二个弹性容器之间唯一的真正区别在于,一个被设置为display: flex,另一个被设置为display: inline-flex。在第一个容器中,<div>成为一个内部具有弹性布局的块级盒子。而在第二个容器中,<div>成为一个内部具有弹性布局的行内块级盒子。

重要的是要记住,一旦你将一个元素设置为弹性容器,比如在图 11-1 中的<div>元素,它将仅对其直接子元素进行弹性布局,而不会影响更深层次的后代元素。不过,你也可以将这些后代元素设为弹性容器,从而实现一些非常复杂的布局。

在弹性容器内部,项目沿着主轴对齐。主轴可以是水平的也可以是垂直的,因此你可以将项目排列成列或行。主轴的方向性由书写模式设置:关于这个主轴概念将在“理解轴”中深入讨论。

如图 11-1 中的第一个<div>所示,当弹性项目未填满容器的主轴(在本例中为宽度)时,它们会留下额外的空间。某些属性规定了如何处理这些额外的空间,我们将在本章后面进行探讨。你可以将子元素分组到左侧、右侧或居中,或者将它们分散开来,定义空间是在子元素之间还是围绕子元素周围扩展。

除了分配空间外,你还可以允许弹性项目扩展以占用所有可用空间,通过在弹性项目之间或在所有弹性项目之间分配额外空间来实现。如果没有足够的空间容纳所有弹性项目,你可以使用 flexbox 属性来指定它们在容器内如何收缩,或者它们是否可以换行到多个弹性行中。

此外,子元素可以相对于其容器或彼此对齐;可以位于容器的底部、顶部或中心;或者拉伸以填充容器。无论同级容器之间内容长度的差异如何,使用 flexbox 可以通过单个声明使所有兄弟元素大小相同。

一个简单的示例

假设我们想要从一组链接创建一个导航栏。这正是 flexbox 设计用来处理的情况。考虑以下内容:

nav {
  display: flex;
}
<nav>
   <a href="/">Home</a>
   <a href="/about">About</a>
   <a href="/blog">Blog</a>
   <a href="/jobs">Careers</a>
   <a href="/contact">Contact Us</a>
</nav>

在上述代码中,将其display属性设置为flex后,<nav>元素被转换为弹性容器,其子链接都成为了弹性项目。这些链接仍然是超链接,但现在它们也是弹性项目,这意味着它们不再是行内级别的框,而是参与其容器的弹性格式化上下文。因此,在 HTML 中<a>元素之间的空白在布局上完全被忽略了。如果你以前使用 HTML 注释来消除链接、列表项或其他元素之间的空格,那么你就明白这有多重要了。

所以让我们为这些链接添加一些 CSS 样式:

nav {
  display: flex;
  border-block-end: 1px solid #ccc;
}
a {
  margin: 0 5px;
  padding: 5px 15px;
  border-radius: 3px 3px 0 0;
  background-color: #ddaa00;
  text-decoration: none;
  color: #ffffff;
}
a:hover, a:focus, a:active {
  background-color: #ffcc22;
  color: black;
}

现在我们有了一个简单的选项卡导航栏,如图 11-2 所示。

一个简单的选项卡导航 (files/nav_displayflex.html)

图 11-2. 一个简单的选项卡导航

现在可能看起来并不起眼,因为在旧式 CSS 中你可以做到这一切。等等:它会变得更好。

从设计上看,flexbox 是方向不可知的。这与块级或行内布局不同,后者分别被定义为垂直和水平倾向。Web 最初是为在监视器上创建页面而设计的,并假定具有无限垂直滚动的水平约束。这种垂直倾向布局对于根据用户代理和视口方向变化、增长和收缩的现代应用程序以及根据语言改变书写模式的情况是不足够的。

多年来,我们一直在开玩笑地谈论垂直居中和多列布局的挑战。有些布局并不好笑,比如确保多个并排框的高度相等,按钮或“更多”链接固定在每个框的底部(图 11-3);或者,保持单个按钮的各部分整齐排列(图 11-4)。Flexbox 让以前具有挑战性的布局效果变得相当简单。

使用 flexbox 的电力网格布局,按钮底部对齐

图 11-3. 使用 flexbox 的电力网格布局,按钮底部对齐

垂直居中排列的多组件按钮

图 11-4. 具有多个组件的小部件,所有内容垂直居中

经典的“圣杯”布局,带有标题、三个高度相等但灵活性不同的列和页脚,可以用几行 CSS 代码使用 flexbox 或网格布局在下一章中介绍的方式创建。以下是可能代表这种布局的 HTML 示例:

<header>Header</header>
<main>
  <nav>Links</nav>
  <aside>Aside content</aside>
  <article>Document content</article>
</main>
<footer>Footer</footer>

随着本章的进行,请记住,flexbox 是为特定类型的布局设计的,即单维内容分布。它最擅长沿着单个维度或轴排列信息。虽然你可以使用 flexbox 创建类似网格的布局(二维对齐),但这不是其预期目的,对于这种用例它存在显著缺陷。如果你发现自己渴望二维布局功能,请参阅第十二章。

弹性容器

完全理解的第一个重要概念是 flex 容器,也称为 容器盒子。应用了 display: flexdisplay: inline-flex 的元素成为 flex 容器,并为其子节点生成 flex 格式化上下文

这些子项是 flex 项,无论它们是 DOM 节点、文本节点还是生成的内容伪元素。flex 容器的绝对定位子项也是 flex 项,但每个子项的大小和位置都像是其所在 flex 容器中唯一的 flex 项一样。

我们将首先查看适用于 flex 容器的所有 CSS 属性,包括几个影响 flex 项布局的属性。然后我们将探讨同样重要的 flex 项概念在“Flex 项”中。

使用 flex-direction 属性

如果你希望布局从上到下、从左到右、从右到左,甚至从底部到顶部,可以使用 flex-direction 控制 flex 项排列的主轴方向。

flex-direction 属性指定了如何在 flex 容器中放置 flex 项。它定义了 flex 容器的主轴,即沿着哪个主轴排列 flex 项(有关更多细节,请参见“理解轴”)。

假设以下基本标记结构:

<ol>
   <li>1</li>
   <li>2</li>
   <li>3</li>
   <li>4</li>
   <li>5</li>
</ol>

图 11-5 显示了应用 flex-direction 四个值排列简单列表的方式,假设是从左到右的语言。

flex-direction 属性的四个值

图 11-5. flex-direction 属性的四个值

默认值 row 看起来与一堆内联元素或浮动元素没有太大区别。这是误导性的,原因很快你就会看到,但请注意其他 flex-direction 值如何影响列表项的排列。

例如,你可以通过 flex-direction: row-reverse 来反转项目的布局。当设置 flex-direction: column 时,flex 项从上到下排列,如果设置 flex-direction: column-reverse,则从下到上排列,如图 11-5 所示。

我们指定了从左到右的语言,因为row的主轴方向——即 flex 项排列的方向——是当前书写模式的方向。稍后我们将讨论书写模式如何影响 flex 方向和布局。

警告

不要使用 flex-direction 来更改从右到左的语言的布局。而是在 HTML 中使用 dir 属性,或者在“设置书写模式”中描述的 writing-mode CSS 属性来指示语言方向。要了解更多有关语言方向和 flexbox 的信息,请参见“处理其他书写方向”。

column 值将 flex 容器的主轴设置为与当前书写模式的块轴相同的方向。在水平书写模式(如英语)中,这是垂直轴,在传统日语等垂直书写模式中,这是水平轴。

因此,在英语(或具有相同书写方向的语言)中声明 column 方向时,flex 项目以与源文档中声明的相同顺序显示,但从上到下而不是从左到右排列,因此 flex 项目是按顺序堆叠而不是并排放置。考虑以下情况:

nav {
  display: flex;
  flex-direction: column;
  border-right: 1px solid #ccc;
}

因此,通过简单地编写几个 CSS 属性,我们可以为早期看到的横向选项卡列表创建一个漂亮的侧边栏样式导航。对于新布局,我们将 flex-direction 从默认值 row 更改为 column,并将边框从底部移到右侧;图 11-6 显示了结果。

更改 flex-direction 可完全改变内容的布局

图 11-6. 更改 flex-direction 可完全改变内容的布局

column-reverse 值类似于 column,但主轴是反向的;因此,main-start 放在主轴的末端,而 main-end 放在主轴的起始。在从上到下的书写模式中,这意味着 flex 项目是向上排列的,如之前在图 11-5 中所示。 -reverse 值仅改变外观。键盘导航的标签顺序与底层标记相同。

到目前为止,我们展示的内容非常强大,使得许多布局变得轻而易举。如果我们在完整文档中包含导航,我们可以看到仅凭几个 flexbox 属性声明就可以实现多么简单的布局。

让我们稍微扩展一下前面的 HTML 示例,并将导航作为主页中的一个组件包含进来:

<body>
  <header>
    <h1>My Page's title!</h1>
  </header>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
    <a href="/blog">Blog</a>
    <a href="/jobs">Careers</a>
    <a href="/contact">Contact Us</a>
  </nav>
  <main>
     <article>
       <img alt="" src="img1.jpg">
       <p>This is some awesome content that is on the page.</p>
       <button>Go Somewhere</button>
     </article>
     <article>
       <img alt="" src="img2.jpg">
       <p>This is more content than the previous box, but less than
       the next.</p>
       <button>Click Me</button>
     </article>
     <article>
       <img alt="" src="img3.jpg">
       <p>We have lots of content here to show that content can grow, and
       everything can be the same size if you use flexbox.</p>
       <button>Do Something</button>
     </article>
  </main>
  <footer>Copyright &#169; 2023</footer>
</body>

通过添加几行 CSS,我们可以得到一个布局良好的主页(图 11-7):

* {
  outline: 1px #ccc solid;
  margin: 10px;
  padding: 10px;
}
body, nav, main, article {
  display: flex;
}
body, article {
  flex-direction: column;
}

使用 +flex-direction:+ +row+ 和 +column+ 的主页布局

图 11-7. 使用 flex-direction: rowcolumn 的主页布局

是的,元素既可以是 flex 项目,同时也可以是 flex 容器,就像您在此示例中看到的导航、主体和文章元素一样。 <body><article> 元素将 column 设置为它们的 flex 方向,而我们让 <nav><main> 默认为 row。而且所有这些只需两行 CSS 就能完成!

为了明确起见,在图 11-7 中还有更多样式的应用。我们给所有元素都添加了边框、外边距和内边距,这样你可以通过视觉区分弹性项目以便学习(我们不会将这种不太吸引人的网站投入生产!)。否则,我们只是简单地声明了 body、导航、主体和文章作为弹性容器,使得导航链接、主体、文章、图像、段落和按钮成为弹性项目。

处理其他写作方向

如果你在使用英语或其他从左到右(LTR)语言创建网站,你可能希望弹性项目从左到右、从上到下布局。默认值row可以实现这一点。如果你使用阿拉伯语或其他从右到左(RTL)语言,你可能希望弹性项目从右到左、从上到下布局。默认值row也能够实现这一点。

使用flex-direction: row将弹性项目按照文本方向(也称为写作模式)的相同方向排列,无论语言是 RTL 还是 LTR。尽管大多数网站使用从左到右的语言呈现,有些网站使用从右到左的语言,还有一些是从上到下的。使用 flexbox,当你改变写作模式时,flexbox 会帮助你改变弹性方向。

写作模式由writing-modedirectiontext-orientation属性设置,或者由 HTML 中的dir属性设置。(这些内容在第十五章中有详细介绍。)当写作模式为从右到左时,主轴的方向——因此弹性容器内的弹性项目——在flex-directionrow时将从右到左进行。这在图 11-8 中有示例。

当方向为从右到左时,使用显示的的四个值。

图 11-8. 当写作方向为从右到左时flex-direction的四个值
注意

如果 CSS 中的direction值与元素上的dir属性值不同,则 CSS 属性值优先于 HTML 属性。规范强烈建议使用 HTML 属性而不是 CSS 属性。

竖排文字包括注音符号、埃及象形文字、平假名、片假名、汉字、韩文、麦洛莱迪书写体和象形文字、蒙古文、欧甘文、古代突厥文、八思巴文、彝文,有时还包括日语。这些语言只有在指定了竖排写作模式时才会竖排显示,否则均视为水平排列。

对于自上而下的语言,writing-mode: horizontal-tb 生效,这意味着主轴顺时针旋转 90 度,从默认的左到右变为从上到下,而 flex-direction: column 则从右到左进行。图 11-9 展示了各种 flex-direction 值对以下标记的影响:

<ol lang="jp">
    <li>一</li>
    <li>二</li>
    <li>三</li>
    <li>四</li>
    <li>五</li>
</ol>

水平-tb 写入模式时 flex-direction 的四个值

图 11-9. 写入模式为 horizontal-tb 时 flex-direction 的四个值

是的,行是垂直的,列是水平的。不仅如此,基本的 column 方向是从右到左,而 column-reverse 则是从左到右。这就是将这些值应用于自上而下、从右到左语言时所产生的结果。

好了,您已经看到了 flex 方向和书写模式相互作用的各种方式。但到目前为止,所有示例都显示了单行或单列的 flex 项。当 flex 项的 主尺寸(它们的组合内联尺寸用于 row 或组合块尺寸用于 column)不能适合 flex 容器时会发生什么?我们可以让它们溢出容器,或者允许它们换行到额外的 flex 行上。此外,我们将稍后讨论如何允许 flex 项收缩(或扩展)以适应容器。

包裹 Flex 行

如果所有 flex 项不适合 flex 容器的主轴,则默认情况下 flex 项不会自动换行,也不一定会调整大小。相反,如果通过 flex 项的 flex 属性允许的话,flex 项可能会收缩;否则,flex 项将溢出边界容器框。

您可以影响此行为。flex-wrap 属性设置一个 flex 容器是限制为单行还是在需要时允许多行。

flex-wrap 属性设置为通过 wrapwrap-reverse 允许多个 flex 行时,它确定额外的 flex 项行在原始 flex 项行之前或之后出现的位置。

图 11-10 展示了 flex-direction 值为 row(语言为 LTR 时)时 flex-wrap 属性的三个值的示例。在这些示例中显示了两个 flex 行,第二行及后续 flex 行沿着交叉轴的方向添加(在这种情况下是垂直轴)。

flex-wrap 属性的三个值

图 11-10. 在行定向流中 flex-wrap 属性的三个值

当设置 wrap 时,交叉轴与 flex-direction: rowrow-reverse 的块轴相同,并且与语言的内联轴相同,例如 flex-direction: columncolumn-reverse

不同之处在于,当 flex-wrap 设置为 wrap-reverse 时,交叉轴方向被反转:在 rowrow-reverse 的情况下,后续的 flex 行位于前一行的上方,在 column-reverse 的情况下位于前一列的左侧(假设是像英语这样的从左到右的语言)。

我们稍后会详细讨论轴,但首先让我们谈谈将 flex 方向和包裹整合在一起的缩写属性。

定义灵活的流动

flex-flow 属性允许您定义主轴和交叉轴的包裹方向,以及 flex 项目在需要时是否可以换行到多行。

flex-flow 缩写属性设置 flex-directionflex-wrap 属性,定义 flex 容器的包裹和主轴与交叉轴。

只要 display 设置为 flexinline-flex,省略 flex-flowflex-directionflex-wrap 与声明以下三者之一是相同的,所有这些选项的结果如 图 11-11 所示:

flex-flow: row;
flex-flow: nowrap;
flex-flow: row nowrap;

flex-flow: row;

图 11-11. 一个面向行的未包裹的 flex 流

在从左到右的书写模式中,声明列出的任何属性值,或完全省略 flex-flow 属性,都将创建一个不换行的水平主轴的 flex 容器。图 11-11 展示了沿水平轴分布的 flex 项目,一行内溢出了宽度为 500 像素的容器。

如果我们想要一个反向列导向的流动并且带有包裹,以下任何一种都足够:

flex-flow: column-reverse wrap;
flex-flow: wrap column-reverse;

在像英语这样的从左到右的语言中,这导致 flex 项目从底部向顶部流动,从左侧开始,并在右向方向上换行到新列。在像日语这样的垂直书写模式中,列会是水平的,从右向左流动,并在顶部向底部换行。

我们一直在使用像主轴交叉轴这样的术语,但没有真正深入探讨它们的含义。现在是时候澄清一切了。

理解轴

首先:flex 项目沿着主轴布局。flex 行根据交叉轴方向添加。

在我们介绍 flex-wrap 之前,所有示例都是单行的 flex 项目。在这一行中,flex 项目沿着主轴,在主方向上从主起点到主终点布局。当我们添加 flex 包裹时,新的 flex 行被添加到交叉轴上,在交叉方向上从交叉起点到交叉终点。

正如您所见,该段落中使用了许多术语。这里是一些快速定义:

主轴

  • 内容流动的轴。在伸缩盒中,这是伸缩项流动的方向。

  • 主尺寸

  • 沿主轴的内容总长度。

  • 主轴起点

  • 内容开始流动的主轴端点。

  • 主轴终点

  • 内容流向的主轴端点,与主轴起点相反。

  • 交叉轴

  • “堆叠”伸缩线的轴。在伸缩盒中,这是放置新伸缩项行的方向,如果允许伸缩换行。

  • 交叉尺寸

  • 沿交叉轴的内容总长度。

  • 交叉起点

  • 交叉轴的边缘,从此开始堆叠块。

  • 交叉终点

  • 与交叉起点相对的交叉轴的边缘。

  • 尽管这些术语听起来像逻辑属性,比如 margin-inline-start,但它们并不是同一回事。在这里,每个术语在布局上下文中的意义取决于 flex-direction 的组合、伸缩换行和书写模式的值。为每种书写模式的所有组合绘制图表会变得困难,因此让我们来看看它们在 LTR 语言中的含义。

- 注意
  • 重要的是要理解,当书写方向被反转时,方向也会被反转。为了更简单地解释(和理解)伸缩布局,本章节中其余的解释和示例基于 LTR 书写模式,但会包括书写模式对讨论的伸缩属性和特性的影响。

  • 当考虑 flex-direction 时,我们知道伸缩项将从伸缩容器的主轴开始布局,从主轴起点边缘向主轴终点边缘前进。如果使用 flex-wrap 属性允许容器在伸缩项无法放入一行时换行,那么伸缩线将从交叉起点边缘开始布局,向交叉终点边缘前进。

如图 11-12 所示,当我们有水平排列的伸缩项时,交叉轴是垂直的。在这些示例中,对于水平语言,使用 flex-flow: row wrapflex-flow: row-reverse wrap 设置时,新的伸缩线会添加到前面的伸缩线下方。交叉尺寸与主尺寸相反,对于 rowrow-reverse 伸缩方向,无论是 RTL 还是 LTR 语言,高度是主尺寸,而对于 columncolumn-reverse 方向,则是宽度。

  • 相比之下,wrap-reverse 值颠倒了交叉轴的方向。通常对于 rowrow-reverseflex-direction,交叉轴从上到下,交叉起点在顶部,交叉终点在底部。当 flex-wrapwrap-reverse 时,交叉起点和交叉终点的方向被交换,交叉起点在底部,交叉终点在顶部,交叉轴从底部到顶部。额外的伸缩线添加在前一行的顶部或上方。

当设置 flex-wrap: wrap 时,行和行反向上的弹性线

图 11-12. 行向弹性行的堆叠

如果 flex-direction 设置为 columncolumn-reverse,默认情况下,交叉轴从左到右在从左到右的语言环境中进行,新的弹性行被添加到之前的行右侧。如 图 11-13 所示,当 flex-wrap 设置为 wrap-reverse 时,交叉轴被反转,交叉起始位于右侧,交叉末端位于左侧,交叉轴从右到左,额外的弹性行被添加到之前绘制行的左侧。

当设置 flex-wrap: wrap-reverse 时,列和列反向上的弹性线

图 11-13. 列向弹性行的堆叠

弹性项目的排列

在我们迄今的示例中,我们已经简单讨论了每个弹性行内弹性项目的具体排列方式,以及如何确定这一排列方式。在水平填充一行似乎是直观的,但为什么所有项目都聚集在主起始边缘?为什么不让它们增长以填满所有可用空间,或者在整行中分布?

作为我们这里讨论的一个例子,请查看 图 11-14。注意左上方的额外空间。在此自下而上、从右到左的流程中,新的弹性项目被放置在之前的项目之上,新的换行被放置在每条先前填充行的左侧。

无论 flex 流的值如何,空白空间都将朝主轴末端和交叉轴末端

图 11-14. 空白区域将朝主轴末端和交叉轴末端

默认情况下,无论 flex-flow 的值如何,弹性容器中弹性项目外的空白区域将朝主轴末端和交叉轴末端,但 CSS 具有允许我们改变这一行为的属性。

弹性项目的对齐

在我们迄今的示例中,每当弹性项目未完全填满弹性容器时,所有弹性项目都会集中于主轴的主起始端。弹性项目也可以靠近主轴的主末端,居中,或者以各种方式在主轴上分布。

弹性布局规范为我们提供了弹性容器属性,用于控制空间的分布。justify-content 属性控制弹性行内的弹性项目沿主轴的分布方式。align-items 属性定义了每个弹性行上弹性项目沿交叉轴的默认分布;这一全局默认值可以通过弹性项目的 align-self 属性单独覆盖。当存在多个弹性行并且启用了换行时,align-content 属性定义了这些弹性行在弹性容器的交叉轴上的分布方式。

内容的对齐

justify-content属性使我们能够指导 flex 容器中每个 flex 行沿主轴分布 flex 项目的方式,并处理可能丢失信息的情况。这个属性应用于 flex 容器,而不是单个 flex 项目。

注意

safeunsafe 值,与 CSS Box Alignment Module Level 3 中的其他值一起引入,截至 2023 年初在大多数浏览器中被识别但不被支持。这意味着该值被忽略,但其存在并不使其他声明无效。

图 11-15 展示了在类似英语的书写模式中各种值的效果。

justify-content 属性的值

图 11-15. justify-content 属性的值

使用 startflex-start,flex 项目紧贴主开始。使用 endflex-end,flex 项目朝向主结束对齐。center 选项将项目紧贴在一起,居中于主轴的中间。leftright 选项将项目紧贴在盒子的指定侧,而不考虑实际轴方向。

space-between 值将第一个 flex 项目放置在 flex 行主开始处,并将每个 flex 行中的最后一个 flex 项目放置在主结束处,然后在每对相邻 flex 项目之间放置相等数量的空间。space-evenly 值获取剩余空间并将其分割,以使每个间隙长度相同。这意味着主轴起始和结束边缘的空间大小将与放置在 flex 项目之间的空间相同。

相比之下,space-around将剩余空间分开,然后将每个部分的一半应用于每个 flex 项目,就好像每个项目周围有相等大小的不折叠边距一样。注意,这意味着任意两个 flex 项目之间的间距是 flex 行主开始和主结束处空间的两倍。

stretch 值在 flexbox 中作为 justify-content 的值没有效果。在下一章节中,您将看到它放置在网格容器上时,会导致网格项目在主轴方向上增大,直到占据所有可用空间。

注意

我们将讨论 safeunsafe,它们会影响浏览器如何处理沿交叉轴溢出的项目,在“安全和不安全对齐”中。

对齐和溢出

如果不允许 flex 项目换行到多行并溢出其 flex 行,justify-content 的值将影响 flex 项目溢出 flex 容器的方式。

明确设置justify-content: startflex-start会将 flex 项目分组到主开始的默认行为,并将每个 flex 行的第一个 flex 项目放置在主开始侧。然后,每个后续的 flex 项目都会与前一个 flex 项目的主结束侧对齐。(请记住,主开始侧的位置取决于 flex 方向和书写模式。)如果没有足够的空间放置所有项目并且不允许换行,则项目将溢出到主结束边缘。这在 图 11-16 中有所说明。

图 11-16. start 内容对齐的效果

其镜像是设置justify-content: endflex-end,它将最后一个 flex 放置在与主结束对齐的行上,并使每个前置 flex 项目与后续项目对齐。在这种情况下,如果不允许项目换行,并且没有足够的空间放置所有项目,则项目将溢出到主开始边缘,如 图 11-17 所示。

图 11-17. end 内容对齐的效果

设置justify-content: center会将所有项目紧密对齐,并将它们居中放置在 flex 行的中心,而不是将它们放置在主开始或主结束。如果没有足够的空间放置所有项目并且不允许换行,则项目将均匀地溢出到主开始和主结束边缘。

图 11-18 说明了这些效果。

图 11-18. center 内容对齐的效果

作为leftright值,它们始终从行的左侧或右侧开始打包,而不管轴的方向如何。因此,justify-content: left始终会将基于行的内容对齐到左侧,无论主轴是从左到右还是从右到左。在基于列的内容中,leftstart是一样的,rightend是一样的。任何溢出都将发生在打包开始的相反侧;也就是说,对于justify-content: left,flex 项目将溢出到右侧边缘,对于right则溢出到左侧边缘。

针对这些相对简单的情况,让我们看看会改变 flex 项目之间和周围空间的值,并将它们与它们的换行情况进行比较。请注意,如果允许 flex 项目换行到多行上,则每个 flex 项目周围的空间都基于它们特定 flex 行中的可用空间,并且(在大多数情况下)不会从一行到另一行保持一致。

设置justify-content: space-between会使第一个 flex 项目与主轴起始边缘对齐,最后一个 flex 项目与主轴结束边缘对齐,然后在每个 flex 项目周围均匀分布相等量的空间,直到填满 flex 行(见图 11-19)。如果有三个 flex 项目,则第一个和第二个项目之间的空间与第二个和第三个项目之间的空间相同,但容器的主轴起始边缘和主轴结束边缘与行中的第一个和最后一个 flex 项目之间不会有额外的空白空间。这意味着如果一行只有一个 flex 项目,则它将与主轴起始边缘对齐,而不是居中。如果没有足够的空间来容纳所有的 flex 项目,并且它们不允许换行,则项目将在主轴结束边缘上溢出,产生与justify-content: start视觉上无法区分的效果。

图 11-19. space-between内容对齐的效果

设置justify-content: space-around会均匀分布在每个 flex 项目周围的额外空间,就像主维度两侧的非折叠边距大小相等的元素一样(见图 11-20)。因此,第一个项目与第二个项目之间的空间将是主轴起始边缘与第一个项目之间以及主轴结束边缘与最后一个项目之间的两倍。如果没有足够的空间容纳所有项目,并且它们不允许换行,则项目将在主轴起始边缘和主轴结束边缘上均匀溢出。

图 11-20. space-around内容对齐的效果

设置justify-content: space-evenly意味着用户代理会计算项目数,然后增加一个,然后将行上的任何额外空间分成这么多等份(例如,如果有五个项目,则空间分成六个相等大小的部分);参见图 11-21。空间的一部分被放置在每个列表项之前,就像它是一个非折叠边距一样,并且最后一部分被放置在列表的最后一个项之后。因此,第一个项目与第二个项目之间的空间将与主轴起始边缘与第一个项目之间以及主轴结束边缘与最后一个项目之间的空间相同。如果没有足够的空间容纳所有项目,并且它们不允许换行,则项目将在主轴起始边缘和主轴结束边缘上均匀溢出。

图 11-21. space-evenly内容对齐的效果

stretch 值在作为 justify-content 的值设置在 flex 容器上时没有效果,并且被视为 normal 一样。正如你将在下一章中看到的那样,在放置在网格容器上时,它会导致网格项在主轴方向上增长直至占据所有可用空间。

最后,justify-content: normal 被视为与 justify-content: start 相同。出于历史原因,这也是情况太无聊和太长以至于不在这里讨论,但它意味着 justify-content 的默认值实质上是 start,即使在技术上是 normal

在前面的几个示例中,请注意,当允许 flex 项换行到多行时,每个 flex 行中的空间都基于其特定 flex 行中的可用空间,并且通常不会从一行到另一行保持一致。

justify-content 示例

我们利用了 justify-content 在 图 11-2 中的默认值,创建了一个左对齐的导航栏。通过将默认值更改为 justify-content: flex-end,我们可以在英文中右对齐导航栏:

nav {
  display: flex;
  justify-content: flex-start;
}

注意 justify-content 应用于 flex 容器。如果我们应用于链接本身,使用类似 nav a {justify-content: flex-start;},则不会发生对齐效果。

justify-content 的一个主要优势是,当书写方向改变(比如,对于 RTL 书写模式),我们不必修改 CSS 就可以使标签页到达它们应该去的地方。当应用 flex-start 时,flex 项始终向主轴开始方向分组;在英文中,主轴开始在左侧。对于希伯来文,主轴开始在右侧。如果应用了 flex-end 并且 flex-directionrow,那么在英文中标签页会移动到右侧,在希伯来文中移动到左侧,如图 11-22 所示。

LTR 和 RTL 语言使用 justify-content 的右对齐和左对齐导航

图 11-22. 国际化强大的导航对齐

这可能看起来主轴开始和主轴结束类似于逻辑属性中的内联开始和内联结束。当 flex-direction 设置为 row 时,这种感觉是正确的。然而,当 flex-direction: row-reverse 时,主轴开始和主轴结束会交换,但内联开始和内联结束不会,因为 flex 项的内联方向即使其 flex 顺序改变也保持不变。

我们可以像在 图 11-23 中显示的那样将导航居中。

nav {
  display: flex;
  justify-content: center;
}

使用一个属性值对改变布局

图 11-23. 使用一个属性值对改变布局

到目前为止,我们展示的所有 flex 项目都是单行高度,因此在交叉维度上与其兄弟 flex 项目的大小相同。在讨论换行 flex 行之前,我们需要讨论如何沿交叉轴对齐具有不同尺寸的项目,这恰当地称为 对齐

对齐项目

justify-content 定义了 flex 项目沿 flex 容器的主轴如何对齐,align-items 属性定义了如何沿其 flex 行的交叉轴对齐 flex 项目。与 justify-content 类似,align-items 应用于 flex 容器,而不是单个 flex 项目。

注意

虽然 align-items 设置了容器内所有 flex 项目的对齐方式,align-self 属性 允许覆盖单个 flex 项目的对齐方式,如您将在 “align-self 属性” 中看到的那样。

在 图 11-24 中,请注意 flex 项目相对于交叉轴的排列方式。(对于 row-flowed flex 容器,交叉轴是块轴,对于 column-flowed flex 容器,交叉轴是内联轴。)

当您有一行或一列 flex 项目时,align-items 属性的值

图 11-24. 行和列的 align-items 属性值

默认值 normal 在 flexbox 中被视为 stretch

对于 stretch,每个 flex 项目的交叉起始边缘都放置在容器的交叉起始边缘上,而交叉结束边缘也放置在容器的交叉结束边缘上。这不考虑每个 flex 项目内内容的大小,因此一个具有短内容(如“One”)的 flex 项目仍将其元素框填充为 flex 容器的交叉轴尺寸。

而对于 center 值,元素框大小正好足以容纳沿交叉轴的内容,且不会更大。因此,flex 项目的交叉起始和结束边缘与容器的交叉起始和结束边缘距离相同,从而在交叉轴上使 flex 项目的框居中于 flex 容器内。

对于各种 startend 值,flex 项目的交叉起始或结束边缘都紧贴在 flex 容器的相应边缘上。有许多方法来表达 startend,主要是出于历史原因,这些原因在这里讨论起来太冗长而痛苦了。

注意,当项目对齐到交叉轴的开始或结束时,默认情况下它们的内联尺寸正好与其内容所需的一样大,不会更宽。就像它们的max-width设置为max-size一样,因此多余的内容可以在伸缩项目内部的多行中换行,但如果不需要换行,则元素的内联尺寸不会填充整个伸缩容器的内联尺寸。这是伸缩项目的默认行为,所以如果你希望伸缩元素填充整个伸缩容器的内联尺寸,就像块级盒子填充其包含块一样,请使用stretch值。

使用baseline时,伸缩项目的第一基线在可能时会彼此对齐,也就是说,当flex-directionrowrow-reverse时。由于每个伸缩项目的字体大小不同,因此每个伸缩项中每行的第一基线也不同。第一个基线与其交叉起始边之间的距离最大的伸缩项目将与该行的交叉起始边紧密对齐。其他伸缩项目将被放置在其第一基线与与交叉起始边紧密对齐的伸缩项目的第一基线对齐的位置(因此彼此的第一基线对齐)。当设置align-items: last baseline;时,情况相反。与其交叉结束边之间距离最大的伸缩项目将与该行的交叉结束边紧密对齐。其他伸缩项目将与其最后基线与与交叉结束边紧密对齐的伸缩项目的最后基线对齐,除非被align-self覆盖(见“align-self 属性”)。由于在列流中没有一种方式可以对齐基线,因此在这些情况下,baseline被视为start,或者在last baseline的情况下视为end

伸缩项的边距和对齐

现在你对每个值的行为有了一个大概的了解,但实际情况比这复杂一些。在接下来的多行align-items示例中,应用了以下样式:

flex-container {
  display: flex;
  flex-flow: row wrap;
  gap: 1em;
}
flex-item {border: 1px solid;}
.C, .H {margin-top: 1.5em;}
.D, .I {margin-bottom: 1em;}
.J {font-size: 3em;}

对于每个伸缩行,交叉轴的起始和结束边缘都用红色虚线和蓝色虚线绘制出来。C、H、D 和 I 框添加了顶部或底部边距。我们在伸缩项之间添加了一个间隔(稍后在本章中讨论),以使图表更易读,这不会影响此情况下align-items属性的影响。J 框的字体大小增加,这也增加了其行高。(当我们讨论baseline值时将会涉及到这一点。)

这些边距对stretchcenter对齐方式的影响在图 11-25 中可见。

css5 1125

图 11-25. 边距对交叉轴对齐的影响

stretch 值如其名,将所有“可拉伸”的伸缩项拉伸至与行中最高或最宽的伸缩项一样高或宽。可拉伸的伸缩项是指沿交叉轴没有设置任何非auto值的尺寸属性的伸缩项。在图 11-25 中,这将是block-sizemin-block-sizemax-block-sizeheightmin-heightmax-height属性。如果全部设置为auto,则伸缩项是可拉伸的;否则,就不是。

假设一个伸缩项是可拉伸的,其交叉起始边缘将与伸缩线的交叉起始边缘对齐,其交叉结束边缘将与伸缩线的交叉结束边缘对齐。具有最大交叉尺寸的伸缩项将保持其默认尺寸,而其他伸缩项将增长到该最大伸缩项的尺寸。

图 11-25 展示的是伸缩项 margins 的外边缘与交叉起始和交叉结束对齐,而不是其边框边缘。这由项 C、D、H 和 I 表示,它们看起来比其它伸缩项小。然而实际上并非如此,只是它们的边距始终是完全透明的,占据了一部分伸展空间。

注意

如果伸缩容器的交叉尺寸受到限制,内容可能会溢出伸缩容器的交叉起始和/或交叉结束边缘。溢出的方向不由 align-items 属性决定,而是由讨论在“对齐伸缩线”的 align-content 属性决定。align-items 属性用于在伸缩线内对齐伸缩项,并不直接影响容器内伸缩项的溢出方向。

基线对齐

baseline 值更加复杂。CSS 提供了两种基线对齐方式,分别用 first baselinelast baseline 表示。还可以使用值 baseline,其等效于 first baseline

对于 baseline(和 first baseline),每行中的伸缩项都对齐到最低的第一基线。对于每条伸缩线,距离其基线和交叉起始边缘之间距离最大的伸缩项,将该边缘对齐到该行的交叉起始边缘,并且所有其他伸缩项的基线都与该伸缩项的基线对齐。

要理解这一点,请看图 11-26 中第一组伸缩项,标记为 baseline(和 first baseline)。对于每条伸缩线,交叉起始和结束边缘分别用实线红色和蓝色标记。每行中伸缩项对齐的基线用点线标记,以及被视为主要基线的元素具有较浅的背景和红色文本。

css5 1126

图 11-26. 基线对齐

在第一行(A 到 E),C 框的第一基线被使用了。这是因为 C 框有顶部边距,所以它的第一基线是距离伸缩行交叉起始边缘最远的。所有其他框(A、B、D 和 E)的第一基线都与 C 的第一基线对齐。

在第二行(F 到 J),H 的第一基线被使用了 —— 再次是因为它的顶部边距 —— 因此 F、G、I 和 J 框的第一基线与 H 的对齐。在这里,我们也可以看到 J 框如何使所有其他框的第一基线对齐,尽管它的字体大小要大得多。

类似的情况发生在标记为last baseline的伸缩项上,只是这里,主导因素是底部边距。第一行的 D 框和第二行的 I 框都有底部边距。在这两种情况下,它们的最后基线距离行的交叉末端边缘最远,因此它们所在行的所有其他伸缩项的最后基线与 D 和 I 的最后基线对齐。虚线显示了每个伸缩行中最后基线的位置。

在许多情况下,first baseline看起来会像start(及其等效项,如flex-start),而last baseline看起来会像end。例如,如果图 11-26中的 C 框没有顶部边距,那么第一行中的所有项目将明显紧贴伸缩行的顶部,而不是推开它。每当伸缩项在其交叉起始侧具有不同的边距、边框、填充、字体大小或行高时,startfirst baseline之间会有差异。同样地,任何交叉末端的边距、边框等都会在last baselineend的结果之间创建差异。

当伸缩项的基线与交叉轴平行时,任何基线值都可以变为start。例如,假设我们将图 11-26中的伸缩容器改为flex-direction: column。现在,交叉轴就像英文文本内部的基线一样是水平的。由于没有办法从列的交叉起始边缘创建偏移量来对齐文本基线,baseline就像start一样被处理;或者在最后基线的情况下,被视为end

安全对齐和不安全对齐

在前面的所有示例中,我们让伸缩容器成为它们需要包含伸缩行的大小;也就是说,我们将它们留在block-size: auto(或者在老式 CSS 术语中是height: auto)。但是,如果伸缩容器的块大小以某种方式受到限制,例如通过网格轨道的大小或给定的显式块大小值,那么safeunsafe关键字就会发挥作用。

如果指定了 safe 对齐,那么每当 flex 项溢出 flex 容器时,该 flex 项被视为其 align-self 被设置为 start。看起来可能是这样:

flex-container {display: flex; height: 10em;
     align-items: safe first baseline;}

另一方面,如果使用 unsafe,则无论会导致 flex 容器溢出什么意思,都会尊重 flex 项的对齐方式。

如果你想知道哪个是默认值,答案是都不是。相反,当未声明安全或不安全对齐时,浏览器应默认为 unsafe 行为,除非这会导致 flex 项溢出其最近祖先滚动容器的可滚动区域,在这种情况下,它们应对齐到距离它们溢出边缘最远的交叉轴边缘。图 11-27 展示了一些示例。

警告

截至 2022 年底,只有 Firefox 浏览器完全支持 safeunsafe 关键字,它们必须首先在值中书写(如本节所示),即使属性的正式语法不要求此位置。所有其他常青浏览器都将这些关键字识别为有效,但它们对布局没有影响。

图 11-27. 安全(safe)不安全(unsafe)对齐方式

align-self 属性

如果你想改变一个或多个 flex 项的对齐方式,但不是全部,你可以在希望以不同方式对齐的 flex 项上使用 align-self 属性。该属性接受与 align-items 相同的值,并用于按每个 flex 项覆盖 align-items 属性的值。

你可以使用 align-self 属性覆盖任何单个 flex 项的交叉轴对齐方式,只要它由元素或伪元素表示。你不能覆盖匿名 flex 项(flex 容器的非空文本节点子节点)的对齐方式。它们的 align-self 始终与其父 flex 容器的 align-items 值匹配。

align-items 的默认值是 stretch,但让我们在以下代码中明确设置这一点,这将允许我们为第二个 flex 项设置不同的 align-self 值,如 图 11-28 所示:

.flex-container {align-items: stretch;}
.flex-container .two {align-self: var(--selfAlign);}

css5 1128

图 11-28. 改变单个 flex 项的对齐方式

所有这些 flex 项都具有 align-self 的默认值 auto,意味着它们继承自容器的 align-items 属性的对齐方式(在本例中为 stretch)。每个示例中的例外是第二个 flex 项,它被赋予了下面显示的 align-self 值。

正如我们所说,align-items 的所有值都可以用于 align-self,包括第一个和最后一个基线对齐的值,safeunsafe 对齐等。

对齐 Flex 行

在几乎所有之前的示例中,弹性容器的交叉尺寸总是尽可能高:容器上没有声明block-sizeheight,因此默认为height: auto。因此,弹性容器会随内容增长。

如果容器的交叉尺寸被设置为特定尺寸,可能会在交叉端有额外的空间,或者没有足够的空间来容纳内容。在这种情况下,CSS 允许我们通过align-content属性来控制弹性行的整体位置。

align-content属性决定了弹性容器中任何额外的交叉方向空间如何在弹性行之间和周围分布。尽管值和概念大致相同,align-content与之前讨论的align-items属性不同,后者决定了每个弹性行内的弹性项的位置。

align-content视为类似于justify-content在弹性容器的主轴上对单个项进行对齐的方式,但它是针对弹性行与容器的交叉轴进行的。此属性适用于多行弹性容器,对不换行和其他单行弹性容器没有影响。

将以下 CSS 视为基础,假设弹性项没有边距:

.flex-container {
  display: flex;
  flex-flow: row wrap;
  align-items: flex-start;
  border: 1px dashed;
  height: 14em;
  background-image: url(banded.svg);
}

图 11-29 展示了与 CSS 一起使用时align-content属性的可能值。我们专注于主要的对齐值,并省略了诸如安全对齐和不安全对齐以及第一个和最后一个基线对齐的示例。

使用高度为 14 ems 时,弹性容器比三个弹性行的默认组合高度更高。考虑到某些弹性项的较大文本和各种填充和边框的位,图 11-29 中每个弹性容器大约有 3 ems 的剩余空间。

不同值的额外空间分布

图 11-29. 主要align-content值的额外空间分布

使用normalstretchcenterstartflex-startendflex-end这些值时,多余的空间分布在弹性行的外部,如图 11-29 所示。这些值的行为与align-items相同。对于值stretch,额外的空间均匀分布到所有弹性行,增加它们的交叉尺寸直到它们的边缘接触。对于其他值,弹性行保持在一起,多余的空间放置在一侧或另一侧。

对于剩余的值,弹性行被分开,并以各种方式分布剩余的空间。假设大约 3 ems 的剩余空间相当于 120 像素。(这是大文本,好吗?)

对于space-between来说,每对相邻的 flex 行之间大约有 60 像素的空间,即剩余的 120 像素的一半。对于space-around,空间均匀分布在每行周围:120 像素被分成三份,因为有三个 flex 行。这会在每个 flex 行的交叉开始和交叉结束两侧各增加 20 像素的非折叠空间(40 像素的一半),所以在相邻 flex 行之间有 40 像素的空间。

对于space-evenly,需要插入四个空格:每个 flex 行之前各一个,最后一个 flex 行之后还有一个额外的空格。对于三行来说,这意味着四个空格,每个空格 30 像素。这将在 flex 容器的交叉开始和交叉结束两侧各放置 30 像素的空间,并在相邻 flex 行之间放置 30 像素的空间。

继续对stretch值的示例,你会注意到stretch值不同:使用stretch,额外的空间均匀分布在 flex 行中,而不是在它们之间。在这种情况下,每个 flex 行增加了 40 像素,导致所有三行的高度均等增加—即额外的空间均等分配,而不是按比例分配,每行增加的量完全相同。

如果没有足够的空间容纳所有行,它们将在交叉开始、交叉结束或两者之间溢出,这取决于align-content属性的值。这在图 11-30 中有所展示,其中带有浅灰色背景的虚线框表示一个短的 flex 容器。(为了更清楚地显示其开始和结束位置,每个 flex 容器都添加了少量内联填充。)

当行溢出容器时,align-content 属性的外观

图 11-30. 每个align-content值对应的 flex 行溢出方向

与图 11-29 之间的 CSS 唯一差异是 flex 容器的高度。在这里,flex 容器的高度已经减少到 7 ems,以便创建不足以容纳其所有 flex 行的 flex 容器(你可能还记得,总共约为 10 ems 的高度)。

当 flex 行溢出 flex 容器时,align-contentnormalstretchstartflex-startbaselinelast baselinespace-between会使它们在交叉结束侧溢出,而值centerspace-aroundspace-evenly则会均匀地在交叉开始和交叉结束两侧溢出。只有align-content: endflex-end会使 flex 行仅在交叉开始侧溢出。

请记住,这些值不是顶部或底部为中心的。如果交叉轴向上移动,align-content: flex-start将从底部开始对齐 flex 行,然后向上工作,可能会溢出顶部(交叉末端)边缘。就此而言,当流向是列时,交叉轴将是水平的,此时交叉起始和结束边缘将是 flex 容器的右边或左边边缘。

使用place-content属性

CSS 提供了一个缩写属性,将align-content(我们刚刚讨论过的)和justify-content合并为一个属性。

您可以提供一个或两个值。如果提供一个值,则place-content将像您设置了align-contentjustify-content为相同值一样。换句话说,以下两个规则是等效的:

.gallery {place-content: center;}
.gallery {align-content: center; justify-content: center;}

该行为的例外情况是值与基线相关,如first baseline。在这种情况下,justify-content的值被设置为start,使得以下两个规则等效:

.gallery {place-content: last baseline;}
.gallery {align-content: last baseline; justify-content: start;}

如果给出两个值,第二个值是justify-content的值。因此,以下两个规则是等效的:

.gallery {place-content: last baseline end;}
.gallery {align-content: last baseline; justify-content: end;}

这基本上就是place-content的全部内容。如果您宁愿通过单个缩写属性来对齐和调整内容,place-content可以做到。否则,请分别使用单独的属性。

第十二章中还介绍了另外两个place-的缩写属性。

在 flex 项之间打开间隙

默认情况下,flex 项呈现为它们之间没有空间。通过justify-content的值或向 flex 项添加边距,可以在项之间显示空间,但这些方法并不总是理想的。例如,边距可能会导致 flex 行换行,当实际上并不需要时,即使使用space-betweenjustify-content值也可能导致没有分隔项的空间。如果有一种方法可以定义基本上是最小间隙大小,那将更容易,多亏了间隙属性。

这些属性中的每一个都在相邻的 flex 项之间插入声明大小的空间。这个空间通常被称为gutter。由于历史原因,默认值normal在 flexbox 和网格容器中等于 0 像素(无空间),在多列布局中等于1 em。否则,您可以提供单个长度或百分比值。

假设我们有一组 flex 项,这些项将包裹到多个 flex 行中,并且我们希望在 flex 行之间打开一个 15 像素的间隙。以下是该 CSS 的示例,详见图 11-31:

.gallery {display: flex; flex-wrap: wrap; row-gap: 15px;}

图 11-31. flex 项行之间的间隙

并未对伸缩项设置任何边距。每个伸缩行(行)之间确实有 15 像素的空间,这要归功于row-gap的值。本质上,row-gap的作用就像被称为block-axis-gap,因此如果书写方式改为像vertical-rl这样,使块轴为水平方向,行将从顶部向底部流动,并且它们之间的间隙将位于它们的右侧和左侧(它们的块起始和块结束侧)。

注意,只有在行之间存在间隙:不会在伸缩容器的伸缩项和块起始和结束边缘之间插入间隙。如果想要在这些容器边缘打开相同大小的间隙,可以写成这样:

.gallery {display: flex; flex-wrap: wrap; row-gap: 15px; padding-block: 15px;}

类似地,我们可以使用column-gap在行内轴上打开伸缩项之间的空间。我们可以修改之前的例子,使项目分开,结果如图 11-32 所示:

.gallery {display: flex; flex-wrap: wrap; column-gap: 15px;}

图 11-32。沿行内轴之间相邻伸缩项的间隙

在此,行内端边的伸缩行上仍然有剩余空间,每行具有其自己的空间量。这是因为伸缩项未被赋予justify-content值,因此它们默认为start。这意味着伸缩项之间的间隙都恰好是 15 像素宽。

如果我们将justify-content的值更改为space-between,那么在任何具有剩余空间的伸缩行中,伸缩项之间的间隙将增加相等的量,这意味着它们将被超过 15 像素分开。如果有一行,其中所有伸缩项和所有间隙的行内尺寸恰好等于伸缩行的行内长度,则每个伸缩项之间将有 15 像素的空间。

这就是为什么row-gapcolumn-gap更像是伸缩项或伸缩行之间的最小分隔距离。这些间隙不算是“剩余空间”,就像伸缩项一样。

在相邻伸缩项的外边距边缘之间插入间隙,因此如果向伸缩项添加边距,两个伸缩项之间的实际可见空间将是间隙的宽度加上边距的宽度。考虑以下情况,该情况在图 11-33 中有图示:

.gallery {display: flex; flex-wrap: wrap; column-gap: 15px;}
.gallery div {margin-inline: 10px;}

图 11-33。间隙和边距结合以打开更多空间

现在,伸缩项之间的开放空间都是 35 像素宽:15 像素来自gap属性,加上在伸缩项上设置的行内侧边距 20 像素(10 + 10)。

到目前为止,我们使用了长度值,但百分比呢?任何百分比值用作间隙将被视为与相关轴上容器尺寸的百分比。因此,给定column-gap: 10%,间隙将是伸缩容器沿行内轴的尺寸的 10%。如果容器沿行内轴宽度为 640 像素,那么列间隙将每个为 64 像素。

处理行可能会有点复杂。如果您定义了一个显式的块大小,则百分比仅是该块大小的百分比。一个 block-size(也可以用 heightwidth 设置)为 25em,而 row-gap10% 意味着行间隙将为 2.5 em 宽。如果块大小恰好大于行的块大小总和,也会发生类似的情况。

但是,当块大小仅由添加在一起的行的块大小决定时,任何百分比值都可能导致循环计算:每次计算都会改变正在计算的值,无限循环。假设一个弹性容器有三个弹性行,每行高度恰好为 30 像素。弹性容器设置其高度为 auto,因此将“包裹”弹性行,使其高度为 90 像素(这里我们假设没有填充,但原则上相同)。row-gap10% 将意味着 9 像素的行间隙,插入 2 个行间隙将增加 18 像素的高度。这将增加容器的高度至 108 像素,使得 10% 宽的间隙现在为 10.8 像素,因此容器高度再次增加,间隙增加,容器高度增加,以此类推…

为了避免这种无限循环的情况,当发生循环计算时,间隙被设置为零宽度,所有人都继续他们的生活。实际上,这意味着行间隙的百分比值仅在一小部分情况下有用,而在列间隙方面则可以更广泛地使用。图 11-34 显示了百分比行间隙的示例。

图 11-34. 具有百分比行间隙的示例,带有和不带有显式容器高度

您可以通过分别提供两个属性来设置弹性容器上的列和间隙行,或者您可以使用简写属性 gap

您只需提供一个值给 gap,在这种情况下,它将用于行间隙和列间隙。如果提供两个值,则第一个值始终用于行间隙,第二个值用于列间隙。因此,通过以下 CSS 您将获得 图 11-35 中显示的结果:

#ex01 {gap: 15px 5px;}
#ex02 {gap: 5px 15px;}
#ex03 {gap: 5px;}

图 11-35. 使用 gap 简写属性设置的行和列间隙
注意

最初的 gap 属性在 CSS 多列中定义,而在 CSS Grid 中定义了额外带连字符的间隙属性 grid-row-gapgrid-column-gapgrid-gap,然后在网格、弹性盒和多列上下文中变得更加通用和可用。浏览器需要将旧属性视为新更通用属性的别名;例如,grid-gapgap 的别名。因此,如果您在遗留 CSS 中找到旧的网格间隙属性,可以将其更改为新名称,否则它们将像您有了一样正常工作。

弹性项目

在前面的部分中,您看到了如何通过为容器设置样式来全局布局弹性容器中的所有弹性项目。弹性盒布局规范提供了几个适用于弹性项目的附加属性。借助这些弹性项目特定的属性,我们可以更精确地控制单个弹性容器的子元素布局。

弹性项目是什么?

正如本章中所见,我们通过将display: flexdisplay: inline-flex添加到具有子节点的元素来创建弹性容器。这些弹性容器的子元素称为弹性项目—无论它们是子元素、非空文本节点还是生成的内容。在图 11-36 中,每个字母都被包裹在自己的元素中,包括单词之间的空格,使得每个字母和空格都成为一个弹性项目。

Items with display: flex; become flex containers, and their non-absolutely positioned children become flex items

图 11-36. 子节点是弹性项目,父节点是弹性容器

当涉及到弹性容器的文本节点子元素时,如果文本节点不为空(包含除空白字符以外的内容),它将被包装在一个匿名弹性项目中,表现得像它的弹性项目兄弟一样。尽管这些匿名弹性项目继承了弹性容器设置的所有弹性属性,就像它们的 DOM 节点兄弟一样,但无法直接通过 CSS 进行定位。我们不能直接在它们上面设置任何弹性项目特定的属性。因此,在以下标记中,两个元素(<strong><em>)以及文本“they’re what’s for”成为弹性项目,总共有三个弹性项目:

<p style="display: flex;">
    <strong>Flex items:</strong> they’re what’s for <em>&lt;br&gt;fast!</em>
</p>

通过::before::after生成的内容可以直接进行样式化;因此,本章讨论的所有属性同样适用于生成的内容和元素节点。

弹性容器中的仅包含空白字符的文本节点将被忽略,就像它们的display属性被设置为none一样,如下面的代码示例所示:

nav ul {
  display: flex;
}
<nav>
  <ul>
    <li><a href="#1">Link 1</a></li>
    <li><a href="#2">Link 2</a></li>
    <li><a href="#3">Link 3</a></li>
    <li><a href="#4">Link 4</a></li>
    <li><a href="#5">Link 5</a></li>
  </ul>
</nav>

在上述代码中,通过设置display属性为flex,无序列表成为了弹性容器,其子列表项全部成为弹性项目。这些列表项作为弹性项目是弹性级盒子—在语义上仍然是列表项,但在呈现上不是列表项。它们也不是块级盒子。相反,它们参与它们容器的弹性格式化上下文。列表项之间和周围的空白—换行符和缩进制表符和/或空格—完全被忽略。链接本身不是弹性项目,但是它们是列表项已成为的弹性项目的后代。

弹性项目特性

弹性项目的外边距不会折叠。floatclear属性对弹性项目没有影响,也不会使弹性项目脱离文档流。实际上,当应用于弹性项目时,floatclear被忽略。(但是,float属性仍然可以通过影响display属性的计算值来影响框生成。)考虑以下内容:

aside {
  display: flex;
}
img {
  float: left;
}
<aside>
    <!-- this is a comment -->
    <h1>Header</h1>

    <img src="images/foo.jpg" alt="Foo Master">
    Some text
</aside>

在这个例子中,aside 是弹性容器。注释和仅包含空格的文本节点被忽略。包含“Some text”文本节点被包裹在一个匿名的弹性项目中。标题、图片和包含“Some text”文本节点都是弹性项目。因为图片是一个弹性项目,所以float被忽略。

尽管图片和文本节点是内联级别节点,但因为它们是弹性项目,只要它们不是绝对定位的,它们就会被块化:

aside {
  display: flex;
  align-items: center;
}
aside * {
  border: 1px solid;
}
<aside>
    <!-- a comment -->
    <h1>Header</h1>

    <img src="images/foo.jpg" alt="foo master">
    Some text <a href="foo.html">with a link</a> and more text
</aside>

这个标记与前一个代码示例类似,但在这个例子中,我们在非空文本节点中添加了一个链接。在这种情况下,我们创建了五个弹性项目,如图 11-37 所示。注释和仅包含空格的文本节点被忽略。标题、图片、链接前的文本节点、链接以及链接后的文本节点都是弹性项目。

图 11-37. 旁注中的五个弹性项目

包含“Some text”和“and more text”的文本节点被包裹在匿名的弹性项目中,用虚线框表示(为了说明目的添加了虚线)。标题、图片和链接是实际的 DOM 节点,可以直接用 CSS 进行样式化,正如您可以看到的边框样式。匿名弹性容器无法直接定位目标,因此只能继承弹性容器的样式。

此外,vertical-align 对弹性项目没有影响,除非它影响了弹性项目内文本的对齐。在弹性项目上设置vertical-align: bottom将使该弹性项目内的所有文本与它们所在行框的底部对齐;它不会将弹性项目推到其容器的底部。(这是align-itemsalign-self的作用。)

绝对定位

虽然float不会使弹性项目实际浮动起来,但设置position: absolute则完全不同。弹性容器的绝对定位子元素,与任何其他绝对定位的元素一样,都会脱离文档流。

更重要的是,它们不参与弹性布局,也不属于文档流。然而,它们可以受到设置在弹性容器上的样式的影响,就像子元素可以受到非弹性容器父元素的影响一样。除了继承任何可继承属性外,弹性容器的属性可以影响定位的原点。

flex 容器的绝对定位子元素受 flex 容器的 justify-content 值和其自身的 align-self 值的影响(如果有的话)。例如,如果在绝对定位的子元素上设置了 align-self: center,它将从 flex 容器父元素的交叉轴中心开始对齐。然后,该元素或伪元素可以通过 topbottom、边距等属性移动。

order 属性(在 “order 属性” 中解释)可能不会影响绝对定位的 flex 容器子元素的绘制位置,但会影响其相对于兄弟元素的绘制顺序。

最小宽度

在 图 11-38 中,您会注意到在 nowrap 默认 flex-wrap 值的容器内,flex 容器内的 flex 行溢出。这是因为当涉及到 flex 项目时,min-width 的隐含值是 auto,而不是 0。最初在规范中,如果项目不能适应单一主轴,它们将会收缩。然而,min-width 属性的规范在应用到 flex 项目时已经改变。(传统上,min-width 的默认值是 0。)

当最小宽度默认为自动时, 项目溢出其容器,除非允许换行

图 11-38. 使用最小宽度 flex 项目的 flex 容器溢出

如果将 min-width 设置为比计算值 auto 更窄的宽度——例如,如果声明 min-width: 0——则 nowrap 示例中的 flex 项目将收缩到比其实际内容更窄(某些情况下)。如果允许项目换行,则它们将尽可能窄以适应其内容,但不会更窄。 图 11-39 描述了这两种情况。

在非换行容器中,如果将最小宽度明确设置为 ,则  项目将会收缩,这是 Safari 9 的默认设置

图 11-39. 非换行和换行 flex 容器中零最小宽度 flex 项目

flex 项目特定属性

flex 项目的对齐方式、顺序和灵活性在其 flex 容器上通过属性设置到一定程度上可控时,对于更细粒度的控制,可以应用于单个 flex 项目的多个属性。

flex 缩写属性及其组成属性 flex-growflex-shrinkflex-basis 控制了 flex 项目的灵活性。灵活性flex 项目沿着主轴能够增长或收缩的量。

flex 属性

flex 布局的定义方面是能够使 flex 项目 灵活:调整它们的宽度或高度以填充主尺寸中的可用空间。 flex 容器按照其 flex 增长因子比例分配剩余空间给其项目,或按其 flex 缩小因子的比例收缩它们以防止溢出(我们马上会探讨这些概念)。

在 flex 项目上声明 flex 简写属性,或者定义构成简写的各个属性,使您能够定义增长和缩小因子。 如果有多余空间,您可以告诉 flex 项目增长以填充该空间。 或者不要。 如果没有足够的空间来容纳所有 flex 项目在其定义或默认大小中,您可以告诉 flex 项目按比例缩小以适应空间。 或者不要。

所有这些都是通过 flex 属性完成的,这是 flex-growflex-shrinkflex-basis 的简写属性。 虽然这三个子属性可以单独使用,但强烈建议始终使用 flex 简写,我们将很快解释原因。

flex 属性指定了灵活长度的组成部分:flex 项目的长度是沿主轴(参见 “理解轴”)的 flex 项目的长度。 当盒子是 flex 项目时,将参考 flex 来确定盒子的尺寸,而不是主轴尺寸维度属性(heightwidth)。 flex 属性的组件包括 flex 增长因子、flex 缩小因子和 flex 基础值。

Flex basis 确定了如何实施 flex 增长和缩小因子。 如其名称所示,flex 简写的 flex-basis 组件是 flex 项目确定可以增长填充可用空间的基础,或者在没有足够空间容纳所有 flex 项目时缩小以适应所有 flex 项目的初始大小。 可以通过指定增长和缩小因子均为 0 来限制到特定大小:

.flexItem {
    width: 50%;
    flex: 0 0 200px;
}

在上述 CSS 中,flex 项目的主轴尺寸将恰好为 200 像素,因为 flex 基础值为 200px,既不允许增长也不允许缩小。 假设主轴是水平的,则会忽略 width 的值 (50%)。 类似地,如果主轴是垂直的,则会忽略 height 的值。

注意

此处对 heightwidth 的覆盖发生在层叠之外,因此甚至不能通过在 flex 项目的 heightwidth 值中添加 !important 来覆盖 flex 基础值。

如果选择器的目标不是 flex 项目,则将 flex 属性应用于其将不会产生任何效果。

理解组成 flex 简写属性的三个组件非常重要,以便能够有效地使用它。

flex-grow 属性

flex-grow 属性定义了当空间可用时,伸缩项是否允许增长,以及如何相对于其他伸缩项兄弟项的增长成比例地增长。

警告

建议通过flex的快捷方式将增长因子声明为flex-grow属性的方式被规范的作者强烈不建议。相反,应将增长因子作为flex的一部分声明。我们仅在此讨论属性以探讨增长的工作方式。

flex-grow 的值始终是一个数字。负数是无效的。您可以使用非整数,只要它们大于或等于 0 即可。该值设置了伸缩增长因子,确定了伸缩项在伸缩容器的剩余空间分配时相对于其他伸缩项兄弟项的增长量。

如果伸缩容器内有任何空间可用,则该空间将根据各个增长因子的值以非零正增长因子比例分配给子项。

例如,假设一个宽度为750px的水平伸缩容器,有三个伸缩项,每个设置为width: 100px。伸缩项占据了 300 像素的空间,剩下 450 像素的“剩余”或可用空间(因为 750 - 300 = 450)。这是图 11-40 中展示的第一个场景:没有任何伸缩项被允许增长。

增长因子为 0 时,伸缩项不会增长;任何正值都将使项目按比例增长

图 11-40. 各种伸缩增长因子场景

在图 11-40 中的第二个场景中,只有一个伸缩项(第三个)被赋予了增长因子。我们给它的声明是flex-grow: 1,但浏览器可以理解的任何正数也可以。在这种情况下,两个没有增长因子的项目和一个有增长因子的第三个项目将所有可用空间都分配给具有增长因子的伸缩项。因此,第三个伸缩项获得了所有 450 像素的可用空间,最终宽度为 550 像素。它在其他样式中应用的width: 100px被覆盖。

在第三和第四个场景中,尽管伸缩增长因子不同,但同一伸缩项的宽度相同。让我们考虑第三个场景,其中增长因子为 1、1 和 3。这些因子相加得到总数 5。然后,每个因子除以总数以得到比例。因此,在这里,三个值分别除以 5,得到 0.2、0.2 和 0.6。

每个比例都乘以可用空间以获取增长量。因此:

  1. 450 px × 0.2 = 90 px

  2. 450 px × 0.2 = 90 px

  3. 450 px × 0.6 = 270 px

这些是添加到每个 flex 项起始宽度的增长部分。因此,最终宽度分别为 190 像素、190 像素和 370 像素。

第四种情况结果相同,因为比例相同。想象一下,我们将增长因子修改为 0.5、1 和 1.5。现在的计算表明,第一个 flex 项获得可用空间的六分之一,第二个获得三分之一,第三个获得一半。这导致最终的 flex 项宽度分别为 175、250 和 425 像素。如果我们声明增长因子为 0.1、0.1 和 0.3,或者 25、25 和 75,或者实际上任何 1:1:3 对应的组合,结果都将相同。

如 “最小宽度” 所述,如果未设置宽度或 flex 基础,则 flex 基础默认为 auto,这意味着每个 flex 项的基础宽度为其非包装内容的宽度。auto 值很特殊:它默认为 content,除非为项目设置了宽度,在这种情况下,flex 基础将成为该宽度。auto 值在 “自动 flex 基础” 中讨论过。如果我们在这个例子中没有设置宽度,根据我们的小字体大小,主轴上的可分配空间将超过 450 像素。

注意

一个 flex 项的主轴大小受到可用空间、所有 flex 项的增长因子以及项的 flex 基础的影响。我们还没有涵盖 flex 基础,但那时会很快的!

现在让我们考虑具有不同 width 值以及不同增长因子的 flex 项。在 图 11-41 中的第二个例子中,我们有宽度为 100 像素、250 像素和 100 像素的 flex 项,其增长因子分别为 1、1 和 3,在一个宽度为 750 像素的容器中。这意味着我们有额外的 300 像素空间要在总共五个增长因子(因为 750 - 450 = 300)之间分配。因此,每个增长因子为 60 像素(300 ÷ 5)。因此,第一个和第二个 flex 项,具有 flex-grow 值为 1,每个将增长 60 像素。最后一个 flex 项将增长 180 像素,因为其 flex-grow 值为 3

可用空间均匀分配给每个增长因子;任何正值都将允许项目按比例增长。

图 11-41。混合宽度和增长因子

总结一下,flex 容器中的可用空间、增长因子和每个 flex 项的最终宽度如下:

  • 可用空间:750 px – (100 px + 250 px + 100 px) = 300 px

  • 增长因子:1 + 1 + 3 = 5

  • 每个增长因子的宽度:300 px ÷ 5 = 60 px

当进行弯曲时,根据它们的原始宽度和增长因子,flex 项的宽度变为

  • 项 1 = 100 px + (1 × 60 px) = 160 px

  • 项 2 = 250 px + (1 × 60 px) = 310 px

  • item3 = 100 px + (3 × 60 px) = 280 px

总计为 750 像素。

生长因子和 flex 属性

flex 属性可以接受最多三个值 — 生长因子、收缩因子和基础值。如果第一个非空的正数数值设置了生长因子(即flex-grow 值)。当在 flex 值中省略生长和收缩因子时,生长因子默认为1。但是,如果既未声明 flex 也未声明 flex-grow,生长因子则默认为0。是的,真的。

回想一下 图 11-40 中的第二个示例,其中 flex 生长因子为 0、0 和 1。因为我们仅声明了 flex-grow 的值,所以 flex 基础值被设置为auto,就像我们声明了以下内容一样:

#example2 flex-item {
  flex: 0 1 auto;
}
#example2 flex-item:last-child {
  flex: 1 1 auto;
}

这意味着前两个 flex 项没有生长因子,有收缩因子,并且基础值为auto。如果在 图 11-40 的示例中使用 flex 而不是不合理地使用 flex-grow,则每种情况下的 flex 基础值都将被设置为0%,就像这样做了一样:

#example2 flex-item {
  flex: 0 1 0%;
}
#example2 flex-item:last-child {
  flex: 1 1 0%;
}

由于收缩因子默认为1,基础默认为0%,以下的 CSS 与前面的代码段完全相同:

#example2 flex-item {
  flex: 0;
}
#example2 flex-item:last-child {
  flex: 1;
}

这将导致 图 11-42 中显示的结果。与 图 11-40 进行比较,看看事情如何变化(或未变化)。

您可能会注意到前两种情况中的一些奇怪之处:flex 基础值被设置为 0,而第二种情况中仅最后一个 flex 项具有正值的 flex 生长。逻辑似乎应该表明三个 flex 项的宽度分别为 0、0 和 750 像素。但逻辑也表明,如果 flex 容器有足够的空间容纳所有内容,即使基础值设置为0,也不应该使内容溢出其 flex 项。

规范的作者们考虑到了这个困境。当 flex 属性声明显式设置或默认设置 flex 基础值为0% 并且 flex 项的生长因子为0 时,非生长型 flex 项的主轴长度将会收缩到内容允许的最小长度,甚至更小。在 图 11-42 中,这个最小长度是最宽的字符序列 “flex:” 的宽度(包括冒号)。

只要 flex 项具有可见的溢出并且没有显式设置的 min-width(或垂直主轴的 min-height),最小宽度(或最小高度)将是 flex 项需要的最小宽度(或高度),以适应内容或声明的 width(或 height),以较小者为准。

当 flex 基础值为 0 且某些项不允许生长时,flex 生长看起来有所不同

图 11-42. 使用 flex 快捷方式时的 flex 大小调整

如果允许所有项目增长,并且每个 flex 项的 flex 基础是0%,则所有空间而不仅仅是多余的空间都按照增长因子的比例进行分配。在图 11-42 的第三个示例中,两个 flex 项的增长因子为 1,而一个 flex 项的增长因子为 3。因此,总共有五个增长因子:

  • (2 × 1) + (1 × 3) = 5

有五个增长因子,并且总共有 750 像素,每个增长因子值 150 像素:

  • 750 像素 ÷ 5 = 150 像素

虽然默认的 flex 项大小为 100 像素,但 0% 的 flex 基础覆盖了该值,使得我们有两个 flex 项每个为 150 像素,最后一个 flex 项宽度为 450 像素:

  • 1 × 150 像素 = 150 像素

  • 3 × 150 像素 = 450 像素

类似地,在图 11-42 的最后一个示例中,两个具有 0.5 的 flex 项的增长因子,以及一个具有 1.5 的 flex 项,总共有 2.5 的增长因子:

  • (2 × 0.5) + (1 × 1.5) = 2.5

有 2.5 的增长因子,并且总共有 750 像素,每个增长因子值 300 像素:

  • 750 像素 ÷ 2.5 = 300 像素

虽然默认的 flex 项大小为 100 像素,但 0% 的 flex 基础覆盖了该值,使得我们有两个 flex 项每个为 150 像素,最后一个 flex 项宽度为 450 像素:

  • 0.5 × 300 像素 = 150 像素

  • 1.5 × 300 像素 = 450 像素

再次强调,这与仅声明flex-grow不同,因为那意味着 flex 基础默认为auto。在这种情况下,只有额外的空间而不是所有空间按比例分配。而使用flex时,flex 基础设置为0%,因此 flex 项按照总空间的比例增长,而不仅仅是剩余空间。图 11-43 说明了这种差异。

当 flex 基础为 0 时,flex grow 看起来不同,并且有时不允许增长

图 11-43。使用flexflex-grow之间的 flex 大小差异

现在让我们谈谈 flex 收缩因子,它在某些方面是 flex 增长因子的反向,但在其他方面又有所不同。

flex-shrink 属性

<*flex-shrink*> 部分的 flex 简写属性指定了 flex shrink 因子。它也可以通过 flex-shrink 属性进行设置。

警告

强烈建议不要通过 flex-shrink 属性声明收缩因子,而是将收缩因子作为 flex 简写的一部分声明。我们在这里讨论该属性仅仅是为了探讨收缩如何工作。

收缩因子确定了当弹性项目无法容纳时,相对于其余弹性项目兄弟会收缩多少,这由它们的内容和其他 CSS 属性定义。在简写的 flex 属性值中省略或者 flexflex-shrink 都被省略时,收缩因子默认为 1。与增长因子一样,flex-shrink 的值始终是一个数字。负数是无效的。如果你愿意,可以使用非整数值,只要它们大于 0

基本上,收缩因子定义了当弹性项目无法容纳时,“负可用空间”如何分配,而且弹性容器也无法增长或换行。参见 图 11-44。

图 11-44 与 图 11-40 类似,只是弹性项目的 width 被设置为 300px 而不是 100 像素。我们仍然有一个 750 像素宽的弹性容器。三个项目的总宽度为 900 像素,这意味着内容的起始宽度比父弹性容器宽出 150 像素。如果项目不允许收缩或换行(参见 “Wrapping Flex Lines”),它们将从固定大小的弹性容器中溢出。这在 图 11-44 的第一个示例中得到了展示:这些项目不会收缩,因为它们的收缩因子为零。相反,它们会溢出弹性容器。

当  值为 0 时,该弹性项目不会收缩;任何正值都将使该项目相对于在同一弹性行上允许收缩的其他弹性项目成比例地收缩

图 11-44. 多种弹性收缩场景

在 图 11-44 的第二个示例中,只有最后一个弹性项目可以收缩。因此,最后一个弹性项目被迫承担使所有弹性项目都能适应弹性容器的所有收缩工作。由于有 900 像素的内容需要适应到我们的 750 像素容器中,我们有 150 像素的负可用空间。没有收缩因子的两个弹性项目保持在 300 像素宽度。第三个弹性项目,其收缩因子为正值,收缩了 150 像素,最终宽度为 150 像素。这使得三个项目能够适应容器内。在这个例子中,收缩因子为 1,但如果是 0.001100314159.65 或任何浏览器能理解的其他正数,结果都将是一样的。

在第三个示例中,我们对所有三个弹性项目都设置了正收缩因子:

#example3 flex-item {
  flex-shrink: 1;
}
#example3 flex-item:last-child {
  flex-shrink: 3;
}

由于这是我们声明的三个 flex 简写属性中的唯一一个,这意味着弹性项目的行为将如同我们声明了以下内容一样:

#example3 flex-item {
  flex: 0 1 auto; /* growth defaults to 0, basis to auto */
}
f#example3 flex-item:last-child {
  flex: 0 3 auto;
}

如果所有的项都允许收缩,如此例所示,那么收缩将按照收缩因子的比例进行分配。这意味着,与其同级伸缩项的收缩因子相比,伸缩项的收缩因子越大,该项在收缩时收缩得越多。

对于一个宽度为 750 像素的父级容器,和三个宽度为 300 像素的伸缩项,需要从可以收缩的伸缩项中刮掉 150 个“负空间”像素(在本例中是全部)。其中,两个伸缩项具有收缩因子 1,一个伸缩项具有收缩因子 3,总共有五个收缩因子:

  • (2 × 1) + (1 × 3) = 5

有五个收缩因子,总共需要从所有伸缩项中削减 150 像素,每个收缩因子相当于 30 像素:

  • 150 px ÷ 5 = 30 px

默认的伸缩项大小为 300 像素,导致我们有两个伸缩项的宽度分别为 270 像素,最后一个伸缩项的宽度为 210 像素,总计 750 像素:

  • 300 px – (1 × 30 px) = 270 px

  • 300 px – (3 × 30 px) = 210 px

以下 CSS 产生了相同的结果:尽管收缩因子的数字表示不同,但它们在比例上是相同的,因此伸缩项的宽度将是相同的:

flex-item {
  flex: 1 0.25 auto;
}
flex-item:last-child {
  flex: 1 0.75 auto;
}

注意,在这些示例中,伸缩项将收缩到分别为 210、210 和 270 像素,只要每个伸缩项内的内容(如媒体对象或不可换行的文本)不超过 210、210 或 270 像素。如果伸缩项包含不能在主尺寸上换行或以其他方式收缩的内容,则该伸缩项将不会进一步收缩。

假设第一个伸缩项包含一张宽 300 像素的图像。第一个伸缩项无法收缩,其他伸缩项可以收缩;因此,它不会收缩,就好像它有一个空的收缩因子。在这种情况下,第一个项将为 300 像素,剩余的 150 像素负空间将根据第二个和第三个伸缩项的收缩因子比例进行分配。

鉴于这种情况,我们有四个未受阻的收缩因子(来自第二个伸缩项的一个,以及第三个伸缩项的三个),用于 150 个负空间像素,每个收缩因子相当于 37.5 像素。伸缩项的宽度最终将分别为 300、262.5 和 187.5 像素,总计 750 像素,如此处所示,并在图 11-45 中进行了说明:

  • item1 = 300 px – (0 × 37.5 px) = 300.0 px

  • item2 = 300 px – (1 × 37.5 px) = 262.5 px

  • item3 = 300 px – (3 × 37.5 px) = 187.5 px

图 11-45. 由于伸缩项内容而受到限制的收缩

如果图像宽度为 296 像素,那么第一个弹性项将能够收缩 4 像素。剩余的 146 像素负空间将分配给剩余的四个因子,每个因子分配 36.5 像素。然后,弹性项宽度将变为 296、263.5 和 190.5 像素。

如果所有三个弹性项包含非换行文本或宽度为 300 像素或更宽的媒体,则这三个弹性项都不会收缩,类似于第一个示例中的图 11-44。

基于宽度和收缩因子的比例缩小

前面的代码示例相对简单,因为所有弹性项的宽度相同。但如果宽度不同呢?如果第一个和最后一个弹性项的宽度为 250 像素,而中间的弹性项的宽度为 500 像素,如图 11-46 所示?

弹性项根据其收缩因子按比例收缩

图 11-46. 弹性项根据其收缩因子按比例收缩

弹性项根据其收缩因子和弹性项的宽度按比例收缩,宽度通常是弹性项内容的宽度,没有换行。在图 11-46 中,我们试图将 1000 像素放入一个 750 像素宽的弹性容器中。我们有多出的 250 像素要从五个收缩因子中移除。

如果这是一个flex-grow的情况,我们只需将 250 像素除以 5,分配每个增长因子 50 像素。如果我们按这种方式收缩,我们得到的弹性项宽度分别为 200、550 和 100 像素。但实际的收缩方式并非如此。

在这里,我们有 250 像素的负空间要按比例分配。为了获取收缩因子的比例,我们将负空间除以弹性项宽度(更精确地说,沿主轴的长度)乘以它们的收缩因子总和:

S h r i n k P e r c e n t = NegativeSpace ((Width1×ShrF1)+...+(WidthN×ShrFN))

使用这个方程式,我们找到了收缩百分比:

  • = 250 px ÷ [(250 px × 1) + (500 px × 1) + (250 px × 3)]

  • = 250 px ÷ 1500 px

  • = 0.166666667 (16.67%)

当我们按flex-shrink值的 16.67%减少每个弹性项时,我们得到如下减少的弹性项宽度:

  • item1 = 250 px × (1 × 16.67%) = 41.67 px

  • item2 = 500 px × (1 × 16.67%) = 83.33 px

  • item3 = 250 px × (3 × 16.67%) = 125 px

然后从起始大小分别减去每个缩小值,得到的弹性项宽度分别为 208.33、416.67 和 125 像素。

不同的基础值

当收缩因子被设置为0,且弹性项的宽度和基础宽度均设为auto时,即使你认为内容应该换行,该项的内容也不会换行。相反,任何正的收缩值都会使内容换行。因为收缩是根据收缩因子成比例的,如果所有弹性项的收缩因子相似,内容应该会以相似的行数换行。

在图 11-47 中显示的三个示例中,弹性项未声明宽度。因此,宽度基于内容,因为width默认为auto。弹性容器的宽度已调整为 520 像素,而不是通常的 750 像素。

css5 1147

图 11-47. 弹性项根据其收缩因子和内容成比例收缩

请注意,在第一个示例中,所有项的flex-shrink值相同,所有内容都会换行为四行。在第二个示例中,第一个弹性项的收缩因子是其他弹性项的一半,所以它将内容换行为(大致)一半的行数。这就是收缩因子的威力。

在第三个示例中,没有收缩因子,文本根本不换行,而且弹性项远超容器。

警告

截至 2022 年末,这种“行平衡”和拒绝换行的行为在各浏览器间并不一致。如果您在自己尝试时看到不同的结果,可能就是原因。

因为flex属性的收缩因子按比例减少弹性项的宽度,当宽度收缩或扩展时,弹性项中的文本行数也会增加或减少,从而在收缩因子相似时在兄弟弹性项内产生类似高度的内容。

在这些示例中,假设弹性项的内容分别为 280、995 和 480 像素,这些是第三个示例中非换行弹性项的宽度(由开发工具测量,然后四舍五入,以使此示例更简单)。这意味着我们必须通过根据其收缩因子成比例收缩弹性项,将 1,755 像素的内容装入 520 像素宽的弹性容器中。我们有 1,235 像素的可用负空间进行成比例分配。

注意

请记住,您不能依赖于 Web 检查工具来确定生产中的收缩因子。我们正在进行此练习,以展示收缩因子的工作原理。如果细节不是您的事情,请随时跳到“flex-basis 属性”。

在我们的第一个示例中,弹性项最终将具有相同或近似相同的文本行数。这是因为弹性项根据其内容的宽度成比例收缩。

我们没有声明任何宽度,因此不能像在前面的示例中那样简单地使用显式元素宽度作为计算基础。相反,我们根据内容的宽度(分别为 280、995 和 480 像素)在 1,235 像素的负空间中按比例分配它们。我们确定 520 是 1,755 的 29.63%。要确定收缩因子为 1 的每个 flex 项目的宽度,我们将每个 flex 项目的内容宽度乘以 29.63%:

  • item1 = 280 px × 29.63% = 83 px

  • item2 = 995 px × 29.63% = 295 px

  • item3 = 480 px × 29.63% = 142 px

使用默认的align-items: stretch(见“对齐项目”),三列布局将具有相等高度的三列。通过对所有 flex 项目使用一致的收缩因子,您可以指示这三个 flex 项目的实际内容应该具有大致相等的高度——尽管这样做,这些列的宽度不一定是均匀的。

在 图 11-47 的第二个示例中,flex 项目的收缩因子并不完全相同。第一个 flex 项目将按比例缩小一半,其他项目将按比例缩小。我们开始时拥有相同的宽度:分别为 280、995 和 480 像素,但它们的收缩因子分别为 0.5、1.0 和 1.0。因为我们知道内容的宽度,可以通过数学方法找到收缩因子(X):

  • 280 px + 995 px + 480 px = 1,615 px

  • (0.5 × 280 px) + (1 × 995 px) + (1 × 480 px) = 1,235 px

  • X = 1,235 px ÷ 1,615 px = 0.7647

现在我们知道收缩因子,我们可以找到最终的宽度。如果收缩因子为 76.47%,item2item3 将以此比例收缩,而 item1 将以 38.23% 收缩(因为其 flex-shrink 值是其他项目的一半)。在每种情况下,收缩量四舍五入至最接近的整数:

  • item1 = 280 px × 0.3823 = 107 px

  • item2 = 995 px × 0.7647 = 761 px

  • item3 = 480 px × 0.7647 = 367 px

因此,flex 项目的最终宽度如下:

  • item1 = 280 px – 107 px = 173 px

  • item2 = 995 px – 761 px = 234 px

  • item3 = 480 px – 367 px = 113 px

这三个 flex 项目的总宽度组合为 520 像素。

添加不同的收缩和增长因子使一切变得不那么直观。这就是为什么您可能希望始终声明 flex 简写,最好为每个 flex 项目设置宽度或基础。如果现在还不理解,不用担心;我们将讨论更多关于 flex-basis 收缩的示例。

响应式 flex

允许 flex 项目按比例收缩可创建响应式对象和布局,无需断点查询,例如,在宽屏上显示为 图 11-48,在窄屏上显示为 图 11-49:

nav {
  flex: 0 1 200px;
  min-width: 150px;
}
article {
  flex: 1 2 600px;
}
aside {
  flex: 0 1 200px;
  min-width: 150px;
}

通过设置不同的增长、收缩、基础和最小宽度值,您可以创建响应式布局,无论是否使用媒体查询

图 11-48。一个宽阔的 flexbox 布局

css5 1149

图 11-49。一个窄的 flexbox 布局

在这个例子中,如果视口宽度大于 1000 像素,只有中间列会增长,因为只有中间列被提供了正增长因子。我们还规定在 1000 像素宽标记以下,所有列都会收缩。

让我们一点点来看。<nav><aside>元素有以下 CSS:

flex: 0 1 200px;
min-width: 150px;

它们不会根据基础增长,但可以以相等的速率收缩。这意味着它们默认将以它们的 flex 基础宽度为宽度。如果它们需要收缩,它们将会收缩到最小宽度150px然后停止收缩。然而,如果其中一个有一个超过 150 像素宽的元素,无论是图像还是文本运行,当它达到该内容位的宽度时,它将停止收缩。假设一个 180 像素的图像放入<aside>元素中。一旦它达到 180 像素宽,它将停止收缩。<nav>将继续缩小到 150 像素。

另一方面,<main>元素有以下样式:

flex: 1 2 600px;

因此,如果有空间,<main>元素可以增长。因为它是唯一可以增长的 flex 项目,它得到所有的增长。假设浏览器窗口宽度为 1300 像素,两侧列将各为 200 像素宽,剩下 900 像素宽度给中间列。在收缩情况下,中间列将比其他两个元素快两倍缩小。因此,如果浏览器窗口宽度为 900 像素,每个侧列将为 175 像素宽,中间列为 550 像素宽。

一旦窗口宽度达到 800 像素,侧列将达到它们的最小宽度值150px。从那时起,任何缩窄将由中间列接管。

为了明确起见,在这些情况下,您并不需要使用像素。甚至可以对各种 flex 基础值使用不同的单位度量。前面的例子可以重写如下:

nav {
  flex: 0 1 20ch;
  min-width: 15vw;
}
article {
  flex: 1 2 45ch;
}
aside {
  flex: 0 1 20ch;
  min-width: 10ch;
}

我们不会在这里详细讨论所有的数学,但一般的方法是在字符宽度上设置 flex 基础值以提高可读性,有些基于字符宽度的下限,有些基于视口宽度。

注意

Flexbox 对于像本节所示的一维页面布局非常有用,只有一行中的三列。对于更复杂的布局或更强大的选项集,请使用网格布局(见第十二章)。

flex-basis 属性

正如您已经看到的,弹性项的大小受其内容和框模型属性的影响,并可以通过flex属性的三个组件来重置。flex属性的<flex-basis>组件定义了弹性项在额外或负空间分配之前的初始或默认大小——在允许弹性项根据增长和收缩因子增长或缩小之前。它也可以通过flex-basis属性设置。

警告

强烈不建议通过flex-basis属性声明弹性基础,这是规范作者本人的建议。相反,应将弹性基础作为flex简写的一部分声明。我们在这里讨论属性只是为了探索弹性基础。

弹性基础通过box-sizing设置弹性项元素框的大小。默认情况下,当块级元素不是弹性项时,大小由其父级、内容和框模型属性确定。当没有显式声明或继承尺寸属性时,默认大小为其独立内容、边框和填充,这是块级元素父级宽度的 100%。

弹性基础可以使用与widthheight属性相同的长度值类型进行定义,例如5vw12%300px

通用关键字initial将弹性基础重置为auto的初始值,因此您可能想要声明auto。反过来,如果width(或height)的值设置为auto,则flex-basis的值被评估为content。这会导致弹性项根据其内容的大小进行调整,尽管规范中没有明确说明具体方法。

内容关键字

除了长度和百分比外,flex-basis还支持min-contentmax-contentfit-contentcontent关键字。我们在第六章中涵盖了前三者,但是在这里我们需要重新审视fit-content,并且需要探索content

当使用fit-content作为flex-basis的值时,浏览器会尽力平衡一行中所有弹性项,使它们在块大小上相似。考虑以下代码,在图 11-50 中有所说明:

.flex-item {flex-basis: 25%; width: auto;}
.flex-item.fit {flex-basis: fit-content;}

图 11-50. fit-content 弹性基础大小

在第一个弹性线上,弹性项的弹性基础被设置为 25%,这意味着每个弹性项从弹性线宽度的 25%作为其尺寸基础开始,并根据浏览器的自由裁量进行弹性调整。在第二个弹性线上,弹性项被设置为使用fit-content作为它们的弹性基础。注意到更多的内容导致弹性项更宽,而内容较少导致更窄的项。

还要注意,尽管不能保证,在某些情况下,一些弹性项目的高度(更正确地说是块大小)可能会不同:比如,其中一个弹性项目的内容换行比其他的多一行。它们应该几乎相同。

这是 Flexbox 的一个优点的良好示例:您可以向布局引擎提供一个总体方向,并让它完成其余工作。在这里,您不需要计算哪些宽度应该分配给哪些弹性项目以平衡它们的高度,只需告诉它fit-content,让它自行处理。

使用content关键字通常会产生与fit-content类似的结果,尽管存在一些差异。一个content基础是弹性项目内容的大小——即内容最长行或最宽(或最高)媒体对象的主轴尺寸长度。这相当于在弹性项目上声明flex-basis: auto; inline-size: auto;

content具有如图 11-51 所示的效果。

css5 1151

图 11-51. 基于content基础的弹性项目尺寸

在第一个和第三个示例中,弹性项目的宽度与内容大小相同;而弹性基础也是相同的尺寸。例如,在第一个示例中,弹性项目的宽度和基础约为 132 像素。三个并排的弹性项目的总宽度为 396 像素,项目之间有一些像素的间距,都轻松适应父容器。

在第三个示例中,我们设置了一个空的收缩因子(0):这意味着弹性项目不能收缩,因此它们不会收缩或换行以适应固定宽度的弹性容器。相反,它们的宽度与非换行文本的宽度相同。这个宽度也是弹性基础的值。三个弹性项目的宽度,因此其基础值分别约为 309 像素、1037 像素和 523 像素。您完全看不到第二个和第三个弹性项目的全部宽度,但它们在章节文件中。

第二个示例与第三个示例包含相同的内容,但弹性项目默认为收缩因子 1,因此此示例中的文本换行。因此,虽然弹性项目的宽度不是内容的宽度,但弹性基础——按比例收缩的基础——是项目内容的宽度。

图 11-51 中的第三个示例也很好地说明了在使用 flex-shrink: 0max-content 关键字会发生什么:每个项目的伸缩基础将是其内容的最大尺寸。如果允许伸缩,浏览器将从每个项目的 max-content 作为基础开始进行伸缩,并从那里开始缩小它们。以下代码捕捉了两者之间的差异,并在 图 11-52 中有所展示:

#example1 {flex-basis: max-content; flex-shrink: 0;}
#example2 {flex-shrink: 1;}

图 11-52. 根据 max-content 基础调整伸缩项的大小,有和没有收缩

在第一个例子中,不允许收缩时,每个伸缩项的宽度都等于其内容的最大宽度而不换行。这导致伸缩项溢出容器(因为 flex-wrap 未设置为 wrap)。在第二个例子中,当 flex-shrink 设置为 1 时,浏览器会等比例缩小每个伸缩项,直到它们都填满了伸缩容器而不溢出。请注意,四个伸缩项中的第二个略微比其他项更高,因为其缩小需要将内容换行至更多行。

对于 min-content 伸缩基础,情况正好相反。请考虑下面的情况,在 图 11-53 中有所展示:

#example1 {flex-basis: min-content; flex-grow: 0;}
#example2 {flex-grow: 1;}

图 11-53. 根据 min-content 基础调整伸缩项的大小,有和没有增长

在第一个例子中,伸缩项尽可能窄以适应其内容。对于包含文本的元素,这使得它们非常高,因为块轴是垂直的。(请注意,第一个例子中伸缩项的全高度已被剪辑,以保持图像尺寸合理。)在第二个例子中,允许伸展伸缩项,因此它们从 min-content 大小开始,并且宽度均匀增加,直到它们全部填满了伸缩容器而不溢出。

在用于创建 图 11-53 的浏览器中,第一个例子中伸缩项的宽度总和约为 361.1 像素(四舍五入到最接近的十分之一像素),每个伸缩项之间有 20 像素的间距。这意味着从第一项的左边缘到最后一项的右边缘大约是 420.1 像素。要得出第二个例子中的结果,考虑到伸缩容器的宽度为 1,200 像素,则容器宽度与内容宽度之间的差值为 1,200 - 420.1 = 778.9 像素。将这一差值除以 4,得到约为 194.7 像素,每个四个伸缩项的宽度增加了该数量。

自动伸缩基础

当设置为 auto 时,无论是显式设置还是默认设置,flex-basis 与元素的主轴尺寸相同,如果元素没有被转换为 flex 项目的话。对于长度值,flex-basis 解析为 widthheight 的值,但当 widthheight 的值为 auto 时,flex-basis 的值回退到 content

当 flex 基础为 auto 且所有 flex 项目都可以适应父 flex 容器时,flex 项目将保持它们的预设尺寸。如果 flex 项目不能适应其父 flex 容器,则这些 flex 项目将根据其非弹性主轴尺寸(除非收缩因子为 0)在父 flex 容器内按比例收缩。

当没有其他属性设置 flex 项目的主轴尺寸(即这些 flex 项目上没有设置 inline-sizemin-inline-sizewidthmin-width),并且设置了 flex-basis: autoflex: 0 1 auto,flex 项目的宽度将仅仅足够容纳内容,如 图 11-54 中的第一个示例所示。在这种情况下,它们是文本flex-basis: auto的宽度,大约为 110 像素。flex 项目保持它们的预设尺寸,就像设置为 display: inline-block 一样。在此示例中,它们位于主轴起点,因为 flex 容器的 justify-content 属性默认为 flex-start

在第二个示例中的 图 11-54,每个 flex 项目都具有 auto 的 flex 基础和明确声明的宽度。如果它们没有被转换为 flex 项目,那么元素的主轴尺寸分别为 100、150 和 200 像素。由于它们在 flex 容器中没有沿主轴溢出,因此它们的尺寸就是这些。

css5 1154

图 11-54. 自动 flex 基础和 flex 项目宽度

在第三个示例中的 图 11-54,每个 flex 项目都具有 auto 的 flex 基础和非常大的明确声明宽度。如果它们没有被转换为 flex 项目,那么元素的主轴尺寸分别为 2,000、3,000 和 4,000 像素。由于它们不可能在不沿主轴溢出的情况下适应 flex 容器,并且它们的 flex 收缩因子都默认为 1,它们会收缩以适应 flex 容器。您可以通过 “不同的基础值” 中概述的过程计算出它们的实际大小;提示:第三个 flex 项目的宽度应从 4,000 像素减少到 240 像素。

默认值

flex-basisflex 都未设置时,flex 项目的主轴尺寸为该项目的预设尺寸,因为默认值是 auto

在 图 11-55 中:弹性基准值默认为 auto,增长因子默认为 0,每个项目的收缩因子默认为 1。对于每个弹性项目,弹性基准值是其单独的 width 值。这意味着弹性基准值被设置为 width 属性的值:在第一个示例中分别为 100、200 和 300 像素,在第二个示例中分别为 200、400 和 200 像素。由于弹性项目的总宽度分别为 600 像素和 800 像素,两者均大于 540 像素宽的主轴容器,它们都在按比例收缩以适应。

当没有设置弹性属性时,弹性项目的主轴尺寸将是项目的预弹性尺寸

图 11-55. 弹性项目的默认大小

在第一个示例中,我们试图将 600 像素放入 540 像素中,因此每个弹性项目将以 10% 的比例收缩,从而得到宽度分别为 90、180 和 270 像素的弹性项目。在第二个示例中,我们试图将 800 像素放入 540 像素中,因此它们都会收缩 32.5%,使得弹性项目的宽度为 135、270 和 135 像素。

长度单位

在前面的例子中,auto 弹性基准值默认为各种弹性项目的声明宽度。CSS 提供其他选项;例如,我们可以使用与 widthheight 相同的长度单位作为我们的弹性基准值。

css5 1156

图 11-56. 使用长度单位弹性基准值调整弹性项目大小

当我们同时具有 flex-basiswidth(或 height,用于垂直主轴)值时,基准值优先于宽度(或高度)。让我们在 图 11-55 的第一个示例中添加基准值。弹性项目包括以下 CSS:

flex-container {
  width: 540px;
}
item1 {
  width: 100px;
  flex-basis: 300px;  /* flex: 0 1 300px; */
}
item2 {
  width: 200px;
  flex-basis: 200px;  /* flex: 0 1 200px; */
}
item3 {
  width: 300px;
  flex-basis: 100px;  /* flex: 0 1 100px; */
}

宽度被基准值覆盖。弹性项目收缩至分别为 270 像素、180 像素和 90 像素。如果容器没有限制宽度,弹性项目将分别为 300 像素、200 像素和 100 像素。

虽然声明的弹性基准可以覆盖弹性项目的主轴尺寸,但其尺寸可能会受到其他属性的影响,例如 min-widthmin-heightmax-widthmax-height。这些属性不会被忽略。因此,例如,一个元素可能有 flex-basis: 100pxmin-width: 500px。尽管弹性基准较小,但将尊重最小宽度为 500px

百分比单位

百分比值对于 flex-basis 是相对于弹性容器主轴尺寸计算的。

我们已经看到 图 11-57 的第一个示例;这里包含它是为了回顾flex-basis: auto文本的宽度在此情况下约为 110 像素。在这种情况下,仅声明 flex-basis: auto 看起来与写 flex-basis: 110px 相同:

flex-container {
  width: 540px;
}
flex-item {
  flex: 0 1 100%;
}

在 图 11-57 的第二个示例中,前两个 flex 项目的 flex-basis 设置为 auto,其默认 widthauto,即相当于它们的 flex-basis 被设置为 content。正如我们之前提到的,这两个项目的 flex-basis 最终等同于 110 像素,因为在这种情况下内容恰好为 110 像素宽。最后一个项目的 flex-basis 设置为 100%

flex-basis 的百分比值是相对于 flex 容器的宽度而言

图 11-57. 使用百分比 flex-basis 值调整 flex 项目大小

百分比值相对于父级元素,即 540 像素。第三个 flex 项目的 basis 设置为 100%,并非唯一位于非换行 flex 容器中的 flex 项目。因此,它不会扩展为父级 flex 容器宽度的 100% 除非 它的收缩因子设置为空收缩因子,表示它无法收缩,或者包含的不换行内容宽度等于或大于父容器宽度。

小贴士

记住:当 flex 基础值为百分比时,主轴大小是相对于父级元素即 flex 容器的。

对于我们的三个 flex 基础值,如果内容确实为 110 像素宽,容器宽度为 540 像素(简化起见,忽略其他盒模型属性),我们有 760 像素要放入 540 像素的空间中。因此,我们需要按比例分配 220 像素的负空间。收缩因子如下:

  • 收缩因子 = 220 px ÷ 760 px = 28.95%

每个 flex 项目将按 28.95% 收缩,变为原本宽度的 71.05%。我们可以计算最终的宽度:

  • item1 = 110 px × 71.05% = 78.16 px

  • item2 = 110 px × 71.05% = 78.16 px

  • item3 = 540 px × 71.05% = 383.68 px

只要 flex 项目不包含比 78.16 像素或 383.68 像素更宽的媒体或不换行文本,这些数字就是准确的。这是这些 flex 项目能够收缩到的最宽程度,只要内容可以折叠到该宽度或更窄。我们说“最宽”,因为如果其他两个 flex 项目无法收缩到这个值那么窄,它们将必须吸收一些负空间。

在 图 11-57 的第三个示例中,flex-basis: auto 项目跨三行。这个示例的 CSS 等同于以下内容:

flex-container {
  width: 540px;
}
item1 {
  flex: 0 1 70%;
}
item2 {
  flex: 0 1 auto;
}
item3 {
  flex: 0 1 80%;
}

我们声明三个 flex 项目的 flex-basis 分别为 70%auto80%。记住,在我们的情况下,auto 是非换行内容的宽度,本例中约为 110 像素,而我们的 flex 容器是 540 像素,基础值等同于以下内容:

  • item1 = 70% × 540 px = 378 px

  • item2 = 文本宽度(“flex-basis: auto”)≈ 110 px

  • item3 = 80% × 540 px = 432 px

当我们将这三个 flex 项目的基础值的宽度相加时,它们总共的宽度为 920 像素,需要适应一个 540 像素宽的 flex 容器。因此,我们有 380 像素的负空间需要在这三个 flex 项目中按比例减少。为了找出比例,我们将我们的 flex 容器的可用宽度除以它们如果不能收缩时的 flex 项目宽度总和:

  • 比例宽度 = 540 px ÷ 920 px = 0.587

因为收缩因子都是相同的,所以这相当简单。每个项目将是其没有 flex 项目兄弟时宽度的 58.7%:

  • item1 = 378 px × 58.7% = 221.8 px

  • item2 = 110 px × 58.7% = 64.6 px

  • item3 = 432 px × 58.7% = 253.6 px

当容器宽度不同时会发生什么?比如,1,000 像素?flex 基础将分别为 700 像素(70% × 1,000 像素)、110 像素和 800 像素(80% × 1,000 像素),总共为 1,610 像素:

  • 比例宽度 = 1,000 px ÷ 1,610 px = 0.6211

  • item1 = 700 px × 62.11% = 434.8 px

  • item2 = 110 px × 62.11% = 68.3 px

  • item3 = 800 px × 62.11% = 496.9 px

因为 70%和 80%的基础值的组合总是大于 100%,无论父级容器有多宽,所有三个项目都会收缩。

如果第一个 flex 项目由于某种原因无法收缩——无论是因为无法收缩的内容,还是其他 CSS 设置其 flex-shrink0——它将是父级宽度的 70%,在这种情况下是 378 像素。其余两个 flex 项目必须按比例收缩以适应剩余的 30%,即 162 像素。在这种情况下,我们预计宽度将为 378 像素、32.875 像素和 129.125 像素。由于文本“basis:”比这更宽——假设 42 像素——我们得到 378 像素、42 像素和 120 像素。图 11-58 展示了结果。

css5 1158

图 11-58。虽然 flex-basis 的百分比值是相对于 flex 容器宽度的,但主轴大小受其兄弟项目的影响

在您的设备上测试此功能可能会有略微不同的结果,因为渲染文本的字体宽度“flex-basis: auto”可能不同,这取决于所使用的字体(我们使用了 Myriad Pro,并回退到 Helvetica 和任何通用无衬线字体)。

零基础

如果既未包括flex-basis属性也未包括flex简写,则flex基础值默认为auto。当包含flex属性但省略了简写中的 flex 基础组件时,基础默认为0。表面上看,你可能会认为auto0两个值相似,但实际上0值非常不同,可能不符合你的预期。

flex-basis: auto的情况下,基础是弹性项目内容的主尺寸。如果每个弹性项目的基础为0,则可用空间是整个弹性容器的主轴尺寸。在任一情况下,可用空间按每个弹性项目的增长因子比例分配。

使用基础值0,弹性容器的大小按比例分配给每个弹性项目,根据其增长因子——默认的原始主轴尺寸定义为heightwidthcontent不计算,尽管min-widthmax-widthmin-heightmax-height会影响弹性尺寸。

如图 11-59 所示,当基础为auto时,只有额外的空间按比例分配并添加到每个设置为增长的弹性项目中。同样地,假设文本宽度为“flex: X X auto”的第一个示例中为 110 像素,则有 210 像素可在六个增长因子中分配,每个增长因子为 35 像素。弹性项目分别为 180、145 和 215 像素宽。

flex-basis auto versus 0

图 11-59。自动和零弹性基础值中的弹性增长

在第二个示例中,当基础为 0 时,宽度的全部 540 像素为可分配空间。在 540 像素的可分配空间中,每个增长因子值为 90 像素。弹性项目分别为 180、90 和 270 像素宽。虽然中间的弹性项目宽度为 90 像素,但本示例中的内容比 110 像素窄,因此弹性项目未换行。

弹性简写

现在你已经更充分地理解了组成flex简写的属性,请记住:始终使用flex简写。它接受通常的全局属性值,包括initialautonone;以及一个整数,通常为1,表示弹性项目可以增长。

弹性值的四个提供最常见的期望效果:

flex: initial

等同于flex: 0 1 auto。这基于inline-size值调整弹性项目的大小(等效于widthheight,取决于内联轴的方向),允许收缩但不允许增长。

flex: auto

等同于flex: 1 1 auto。这基于inline-size值调整弹性项目的大小,使其完全灵活,允许收缩和增长。

flex: none

等同于flex: 0 0 auto。这基于inline-size值调整弹性项目的大小,但使其完全不可伸缩:不能收缩或增长。

flex: <*number*>

等同于flex: <*number*> 1 0。此值将 flex 项目的增长因子设置为提供的<number>。它还将收缩因子和 flex 基础设置为0。这意味着inline-size的值充当最小尺寸,但如果有空间的话,flex 项目将增长。

让我们依次考虑每个问题。

Flexing with initial

全局 CSS 关键字initial可用于所有属性,表示属性的初始值(即规范的默认值)。因此,以下行是等效的:

flex: initial;
flex: 0 1 auto;

声明flex: initial会将增长因子设置为空值,收缩因子设置为1,并将 flex 基础值设置为auto。在图 11-60 中,我们可以看到auto flex 基础值的效果。在前两个示例中,每个 flex 项目的基础是content——每个 flex 项目的宽度都是组成内容的单行字母的宽度。然而,在最后两个示例中,所有项目的 flex 基础值都相等,为 50 像素,因为所有 flex 项目都应用了width: 50pxflex: initial声明将flex-basis设置为auto,正如我们之前看到的,这是width(或height)的值(如果声明),或者如果没有声明则是content的值。

在这些示例的第一和第三个中,我们看到当 flex 容器太小而无法容纳所有 flex 项目以其默认的主轴尺寸时,flex 项目会收缩,以便全部适应父 flex 容器。在这些示例中,所有 flex 项目的组合 flex 基础值大于 flex 容器的主轴尺寸。在第一个示例中,每个 flex 项目的宽度根据其内容的宽度和其收缩能力而异。它们根据其收缩因子成比例地收缩,但不会比其最宽的内容更窄。在第三个示例中,由于width的值,每个 flex 项目的 flex 基础为 50 像素,因此所有项目均等地收缩。

对于具有不同主尺寸的容器,设置了的 flex 项目会收缩但不会增长

图 11-60. 当设置flex: initial时,flex 项目会收缩但不会增长

默认情况下,flex 项目被分组到主轴的起始端,因为flex-startjustify-content属性的默认值。只有当 flex 行中所有 flex 项目的组合主轴尺寸小于 flex 容器的主轴尺寸,并且没有 flex 项目能够增长时,这一点才会显现出来。

Flexing with auto

flex: auto选项类似于flex: initial,但在两个方向上使 flex 项都灵活:如果没有足够的空间容纳所有项,则它们会收缩,并且如果有可分配的空间,则会增长以占据容器内的所有额外空间。flex 项吸收沿主轴的任何自由空间。以下两个语句是等效的:

flex: auto;
flex: 1 1 auto;

图 11-61 展示了使用auto进行各种场景的灵活性。

设置了后,flex 项可以增长和收缩

图 11-61。当设置了flex: auto时,flex 项可以增长和收缩

图 11-61 的第一个和第三个示例与图 11-60 中的示例相同,因为它们的收缩和基础值是一样的。然而,第二个和第四个示例是不同的。这是因为当设置了flex: auto时,增长因子为1,因此 flex 项可以扩展以包含所有额外的可用空间。

使用none防止灵活性

任何flex: none的 flex 项都是不灵活的:它们既不能收缩也不能扩展。以下两行 CSS 是等价的:

flex: none;
flex: 0 0 auto;

图 11-62 展示了none的效果。

设置了后,flex 项既不会增长也不会收缩

图 11-62。使用flex: none,flex 项既不会增长也不会收缩

如图 11-62 的第一个和第三个示例所示,如果空间不足,flex 项将溢出 flex 容器。这与flex: initialflex: auto不同,它们都设置了一个正的收缩因子。

基础解析为auto,这意味着每个 flex 项的主轴尺寸由原本的元素主轴尺寸决定,假如它没有被转换为 flex 项。flex 基础解析为元素的widthheight值。如果该值为auto,则基础值变为内容的主轴尺寸。在前两个示例中,基础值——以及宽度,因为没有增长或收缩——是内容的宽度。在第三和第四个示例中,宽度和基础值都是 50 像素,因为这是应用于它们的width属性的值。

数值灵活性

flex属性的值是单个的正数数值时,该值将用于增长因子,而收缩因子默认为1,基础值默认为0。以下两个 CSS 声明是等效的:

flex: 3;
flex: 3 1 0;

这使得设置了它的 flex 项变得灵活:它可以增长。收缩因子实际上无关紧要:flex 基础被设置为0,因此 flex 项只能从该基础开始增长。

在图 11-63 的前两个示例中,所有伸缩项的伸缩增长因子均为3。伸缩基础为0,因此它们不会“收缩”;它们只是从 0 像素宽度开始等比例增长,直到它们的主轴尺寸之和填充容器沿主轴。所有伸缩项的基础为0时,主尺寸的 100%是可分配空间。第二个示例中伸缩项的主轴尺寸较宽,因为较宽的伸缩容器具有更多可分配空间。

使用 flex: n 时,您声明了伸缩项的增长因子,同时将伸缩基础设置为零

图 11-63. 使用单个数值进行伸缩

任何大于 0 的数值,甚至是 0.1,表示伸缩项可以增长。如果有可用空间进行增长,并且只有一个伸缩项具有正的增长因子,那么该项将占据所有可用空间。如果多个伸缩项可以增长,则可用的额外空间将根据其增长因子按比例分配给每个伸缩项。

图 11-63 的最后三个示例分别声明了六个伸缩项,其值分别为flex: 0flex: 1flex: 2flex: 3flex: 4flex: 5。这些是伸缩项的增长因子,每个都具有收缩因子为1和基础为0。每个的主轴尺寸与指定的伸缩增长因子成比例。你可能会假设在第三个示例中显示文本flex: 0flex: 0项将为 0 像素宽,就像第四个示例中一样——但默认情况下,伸缩项不会收缩到比最长单词或固定尺寸元素的长度更短。

注意

我们向这些图形添加了一些填充、边距和边框,以使视觉效果更加愉悦。因此,最左边的具有声明flex: 0的伸缩项是可见的:它有 1 像素的边框,使其可见,即使宽度为 0 像素。

order 属性

伸缩项默认按照它们在源代码中出现的顺序显示和布局。伸缩项和伸缩行的顺序可以通过flex-direction进行反转,但有时你可能需要一个更复杂的重新排列。order属性可用于更改单个伸缩项的顺序。

默认情况下,所有伸缩项的order属性均为0,所有伸缩项都分配到相同的序号组,并按照它们的源顺序沿主轴方向显示。(这是本章节中所有示例的情况。)

要更改伸缩项的视觉顺序,请将order属性值设置为非零整数。在不是伸缩容器子元素的元素上设置order属性将不会对这些元素产生影响。

警告

改变伸缩项目的视觉渲染顺序会导致元素的源顺序和视觉呈现之间存在差异。如 Mozilla 开发者网络关于 order 的文章所述,“对于低视力用户通过屏幕阅读器等辅助技术进行导航可能产生不利影响。” 对于通过键盘导航并使用放大或其他方式放大页面的用户也可能会造成问题。换句话说:非常小心使用 order,并仅在经过充分的可访问性测试后才在生产环境中使用。

order 属性的值指定了伸缩项目所属的序数组。任何具有负值的伸缩项目在绘制到页面时将会出现在默认为 0 的项目之前,并且所有具有正值的伸缩项目将会出现在默认为 0 的项目之后。虽然视觉上有所改变,但源顺序保持不变。屏幕阅读器和制表顺序保持 HTML 源顺序定义的方式。

例如,如果您有一组 12 个项目,并且希望第七个项目排在第一位,第六个项目排在最后,您可以声明如下:

ul {
  display: inline-flex;
}
li:nth-of-type(6) {
  order: 1;
}
li:nth-of-type(7) {
  order: -1;
}

在这种情况下,我们明确为第六和第七个列表项设置了顺序,而其他列表项默认为 order: 0。图 11-64 展示了结果。

将  设置为非  值将重新排序该伸缩项目

图 11-64. 使用 order 属性重新排序伸缩项目

第七个伸缩项目首先布局,因为 order 属性的负值小于默认的 0,也是其所有同级伸缩项目中最低的值。第六个伸缩项目是唯一具有大于 0 值的项目,因此是其所有同级项目中最高的顺序值。这就是为什么它在所有其他伸缩项目之后布局的原因。所有其他项目,由于默认为 0order,都在第一个和最后一个项目之间按其源顺序绘制。

伸缩容器按照修改后的文档顺序布局其内容,从序数组编号最低的开始向上。当多个伸缩项目具有相同的 order 值时,它们共享一个序数组。每个序数组中的项目将按源顺序显示,序数组按数字顺序从低到高显示。考虑以下情况:

ul {
  display: inline-flex;
  background-color: rgba(0,0,0,0.1);
}
li:nth-of-type(3n-1) {
  order: 3;
  background-color: rgba(0,0,0,0.2);
}
li:nth-of-type(3n+1) {
  order: -1;
  background-color: rgba(0,0,0,0.4);
}

通过为多个伸缩项目设置相同的 order 值,这些项目将按序数组和每个单独序数组内的源顺序显示。图 11-65 展示了结果。

伸缩项目按序数组的顺序以源顺序显示

图 11-65. 弹性项按顺序组出现,按组内源顺序排列

这里发生了什么:

  • 项 2,5,8 和 11 被选中以共享顺序组3,并获得 20%的不透明背景。

  • 项 1,4,7 和 10 被选中以共享顺序组-1,并获得 40%的不透明背景。

  • 项 3,6,9 和 12 根本未被选择。它们默认属于顺序组0

因此,三个顺序组分别是-103。这些组按照这个顺序排列。在每个组内部,项目按照源顺序排列。

此重新排序纯粹是视觉效果。屏幕阅读器应该按照源代码中的顺序读取文档,尽管可能不会。作为视觉变化,弹性项的排序影响页面的绘制顺序:弹性项的绘制顺序是它们出现的顺序,就好像它们在源文档中重新排序了一样,尽管实际上并没有。

使用order属性更改布局对页面的选项卡顺序没有影响。如果图 11-65 中的数字是链接,通过链接的选项卡顺序将按照源代码顺序,而不是按照布局顺序进行。

重新审视选项卡导航

在我们的选项卡导航栏示例中(见图 11-2),我们可以使当前活动选项卡首先显示,如图 11-66 所示:

nav {
  display: flex;
  justify-content: flex-end;
  border-bottom: 1px solid #ddd;
}
a {
  margin: 0 5px;
  padding: 5px 15px;
  border-radius: 3px 3px 0 0;
  background-color: #ddd;
  text-decoration: none;
  color: black;
}
a:hover {
  background-color: #bbb;
  text-decoration: underline;
}
a.active {
  order: -1;
  background-color: #999;
}

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
  `<``a` `class``=``"active"``>``Blog``<``/``a``>`
  <a href="/jobs">Careers</a>
  <a href="/contact">Contact Us</a>
</nav>

改变顺序将改变视觉顺序,但不会改变选项卡顺序

图 11-66. 改变顺序将改变视觉顺序,但不会改变选项卡顺序

当前活动选项卡已添加.active类,并移除了href属性,并将order设置为-1,低于其他兄弟弹性项的默认0,因此它会首先显示。

为什么要移除href属性?因为选项卡是当前活动文档,没有理由文档要链接到自身。但更重要的是,如果它是一个活动链接而不是占位符链接,并且用户正在使用键盘通过导航进行选项卡,出现顺序是博客,主页,关于我们,职业,联系我们,其中博客首先出现;但选项卡顺序将是主页,关于我们,博客,职业,联系我们,遵循源顺序而不是视觉顺序,这可能会令人困惑。

order属性可用于在移动设备和使用屏幕阅读器及其他辅助技术的用户面前将主要内容区域标记为首列,同时创建常见的三列布局外观:中心主内容区域,左侧站点导航和右侧侧边栏,如图 11-48 所示。

虽然你可以在标记中将页脚放在页眉之前,并使用 order 重新排列页面,但这是属性的不当使用。order 属性应仅用于内容的视觉重新排序。你的底层标记应始终反映内容的逻辑顺序。考虑这两种相同内容的标记顺序,这里并排显示以便比较:

<header></header>               <header></header>
<main>                          <main>
   <article></article>            <nav></nav>
   <aside></aside>                <article></article>
   <nav></nav>                    <aside></aside>
</main>                         </main>
<footer></footer>               <footer></footer>

我们一直按照想要它们出现的顺序标记网站,如代码示例中右侧所示,这与我们的三列布局示例中的代码相同(图 11-48)。

如果我们按照左侧显示的方式标记页面,将 <article> 内容(即主内容)放在源代码顺序的第一位,这对于屏幕阅读器、搜索引擎甚至大屏幕上的用户来说更合理,但对于我们的视力用户来说,在较大的屏幕上却在中间位置:

main {
  display: flex;
}
main > nav {
  order: -1;
}

通过使用 order: -1 属性声明,我们能够使 <nav> 出现在首位,因为它是 -1 顺序组中的唯一 flex 项目。<article><aside> 没有显式声明 order,默认为 order: 0

记住,当一个顺序组中有多个 flex 项目时,这些项目按照主轴起点到主轴终点的顺序显示,所以 articleaside 前面显示。

一些开发人员,在至少一个 flex 项目的顺序变更时,喜欢为所有 flex 项目赋予 order 值以提高标记的可读性。我们也可以这样写:

main {
  display: flex;
}
main > nav {
  order: 1;
}
main > article {
  order: 2;
}
main > aside {
  order: 3;
}

在之前的几年里,在浏览器支持 flex 布局之前,这一切都可以通过浮动(floats)来实现:我们会在 <nav> 上设置 float: right。虽然可行,但是使用 flex 布局可以使这种布局变得更加简单,特别是如果我们希望三个列元素——<aside><nav><article>——的高度相等。

概要

使用弹性盒布局,你可以根据多种布局上下文和书写模式响应地布局同级元素,提供了多种选项来安排这些元素并使它们彼此对齐。它使得在父元素内垂直居中元素的任务变得几乎轻而易举,这在 flexbox 出现之前是非常困难的。它还作为普通流和网格布局之间强大而有用的桥梁,是下一章节的主题。

第十二章:网格布局

初创时,CSS 的核心存在一个布局上的空白。设计师将其他功能曲解为布局的目的,特别是floatclear,并通常围绕这个空白进行了各种 hack。Flexbox 布局帮助填补了这个空白,但 Flexbox 实际上是为特定用例设计的,比如导航栏(如第十一章中所示)。

与之相反,网格布局是一种广义的布局系统。通过其对行和列的强调,它可能一开始感觉像是回归到表格布局——在某些方面,这并不算太远——但网格布局比表格布局要丰富得多。网格允许设计的不同部分独立于其文档源顺序布局,甚至可以重叠布局的各个部分,如果您希望如此。CSS 提供了强大灵活的方法来定义网格线的重复模式,将元素附加到这些网格线上,等等。您可以嵌套网格,或者将表格或 flexbox 容器附加到网格上。还有更多更多。

简而言之,网格布局是我们长期等待的布局系统,在 2017 年,它登陆了所有主要的浏览器引擎。它使许多难以或甚至不可能实现的布局变得简单、灵活且健壮。

创建网格容器

创建网格的第一步是定义网格容器。这与定位中的包含块或 flex 布局中的 flex 容器非常相似:网格容器是一个为其内容定义网格格式化上下文的元素。

在这个基础层面上,网格布局与 Flexbox 非常相似。例如,网格容器的子元素成为网格项,就像 Flex 容器的子元素成为 Flex 项一样。这些网格项的子元素不会成为网格元素——尽管任何网格项本身都可以成为网格容器,并且其子元素成为嵌套网格的网格项。可以嵌套网格直至到底层都是网格。

CSS 有两种网格:常规网格和内联网格。这些是使用display属性的特殊值创建的:gridinline-grid。前者生成块级框,后者生成内联级框。图 12-1 说明了它们的区别。

image

图 12-1. 网格和内联网格

这些与blockinline-block值相似,用于display。你创建的大多数网格可能是块级的,尽管在需要时,总是可以创建内联网格。

虽然display: grid创建一个块级网格,但规范明确指出“网格容器不是块容器”。虽然网格框在布局中的参与方式与块容器类似,但它们之间存在差异。

首先,浮动元素不会侵入到网格容器中。实际上,这意味着网格不会像块级容器那样滑动到浮动元素下面。参见图 12-2 以查看差异的演示。

image

图 12-2. 浮动元素与块和网格的不同交互方式

此外,网格容器的边距不会与其后代的边距折叠。这与块级盒子不同,后者的边距(默认情况下)会与后代的边距折叠。例如,有序列表中的第一项可能具有顶部边距,但此边距将与列表元素的顶部边距折叠。网格项目的顶部边距永远不会与其网格容器的顶部边距折叠。参见图 12-3 以说明差异。

image

图 12-3. 边距折叠与不折叠

一些 CSS 属性和功能不适用于网格容器和网格项目:

  • 当应用于网格容器时,所有column属性(例如column-countcolumns等)都会被忽略。(您可以在CSS 多列布局了解更多关于多列属性的信息。)

  • ::first-line::first-letter伪元素不适用于网格容器,会被忽略。

  • 对于网格项目,floatclear实际上被忽略(尽管网格容器不会)。尽管如此,float属性仍然有助于确定子网格容器的display属性的计算值,因为网格项目的display值在它们成为网格项目之前就已解析。

  • vertical-align属性对网格项目的定位没有影响,尽管它可能影响网格项目内部的内容。(不用担心:稍后我们将讨论其他更强大的网格项目对齐方式。)

最后,如果网格容器声明的display属性值为inline-grid 元素被浮动或绝对定位,那么display属性的计算值将变为grid(因此放弃了inline-grid)。

一旦你定义了一个网格容器,下一步是设置内部的网格。然而,在探讨其工作原理之前,有必要先了解一些术语。

理解基本网格术语

我们已经讨论过网格容器和网格项目,但让我们稍微详细定义它们。正如我们之前所说,网格容器是一个创建网格格式化上下文的盒子—也就是说,在这个区域内创建网格,并根据网格布局的规则而不是块布局来布局元素。你可以把它想象成设置为display: table的元素在其中创建表格格式化上下文的方式。考虑到表格的网格性质,这种比较相当合适,但请务必不要假设网格只是另一种形式的表格。网格比表格强大得多。

网格项是在网格格式上下文中参与网格布局的元素。通常情况下,这是网格容器的子元素,但也可以是元素内容中不属于任何元素的匿名文本。考虑下面的情况,其结果显示在图 12-4 中:

#warning {display: grid;
    background: #FCC; padding: 0.5em;
    grid-template-rows: 1fr;
    grid-template-columns: repeat(7, 1fr);}

<p id="warning"><img src="warning.svg"><strong>Note:</strong> This element is a
   <em>grid container</em> with several <em>grid items</em> inside it.</p>

图片

图 12-4. 网格项

注意每个元素、以及元素之间的每一段文本,如何变成了网格项。图像本身也是一个网格项,正如元素和文本一样——总共七个网格项。每个都将参与网格布局,尽管匿名文本段要用各种网格属性来影响会更加困难(甚至不可能)。

注意

如果您想了解 grid-template-rows 和 grid-template-columns,我们将在下一节中详细讨论。

在使用这些属性的过程中,您将创建或引用网格布局的几个核心组件。这些内容在图 12-5 中总结。

最基本的单位是网格线。通过定义一个或多个网格线的位置,您隐含地创建了网格的其余组件:

网格轨道

两个相邻网格线之间的连续行程——换句话说,一个网格列或一个网格行。它从网格容器的一边延伸到另一边。网格轨道的大小取决于定义它的网格线的位置。这些类似于表格的列和行。更通用地说,这些可以被称为块轴和行内轴轨道,在西方语言中,列轨道在块轴上,行轨道在行内轴上。

图片

图 12-5. 网格组件

网格单元格

由四条网格线界定的任何空间,其中没有穿过它的网格线,类似于表格单元格。这是网格布局中最小的区域单位。不能直接使用 CSS 网格属性来定位网格单元格;也就是说,没有属性允许您指定网格项与给定单元格关联。 (但更多细节请见下一点。)

网格区域

由四条网格线限定的任何矩形区域,由一个或多个网格单元组成。一个区域可以小到单个单元格,也可以大到网格中的所有单元格。网格区域可以直接通过 CSS 网格属性进行访问,这些属性允许您定义区域,然后将网格项与其关联。

一个重要的注意点是,这些网格轨道、单元格和区域完全由网格线构成,更重要的是,并不要求所有网格区域都填充有项;可以完全可能使网格的某些甚至大多数单元格为空白。您还可以让网格项彼此重叠,要么通过定义重叠的网格区域,要么通过使用创建重叠情况的网格线引用。

另一个要记住的事实是,您可以根据需要定义多少个网格线。您可以只定义一组垂直网格线,从而创建一堆列和仅一个行。或者您可以走另一条路,创建一堆行轨道而没有列轨道(虽然会有一个,从网格容器的一边延伸到另一边)。

反之,如果您创建了一个条件,阻止网格项放置在您定义的列和行轨道内,或者明确将网格项放置在这些轨道之外,新的网格线和轨道将自动添加到网格中以适应,创建隐式网格轨道(我们将在本章后面返回此主题)。

创建网格线

结果表明,创建网格线可能会变得相当复杂。这不是因为概念难以理解。CSS 只是提供了许多完成任务的方法,每种方法都有其自己微妙不同的语法。

我们将首先看看两个密切相关的属性。

借助这些属性,您可以定义整体网格模板的网格轨道,或者 CSS 规范所称的显式网格。一切都取决于这些网格轨道;如果未正确放置它们,整个布局很容易就会崩溃。

一旦定义了网格轨道,就会创建网格线。如果您为整个网格创建了一个轨道,将创建两条线:一条在轨道开头,一条在末尾。如果有两个轨道,则意味着有三条线:一条在第一个轨道的开头,一条在两者之间,一条在第二个轨道的末尾。依此类推。

提示

当您开始使用 CSS 网格布局时,最好先在纸上或某种接近数字的数字中勾画出网格轨道的位置。有一个视觉参考可以确定线条的位置以及轨道应如何行动,这样写网格 CSS 会更容易些。

<track-list>和<auto-track-list>的确切语法模式复杂且嵌套几层深,拆开它们需要大量时间和空间,最好专注于探索事物如何运作。有许多方法可以实现所有这些,因此在我们开始讨论这些模式之前,我们有一些基本的事情要建立。

首先,网格线始终可以通过数字引用,但也可以由作者明确命名。例如,看看图 12-6 中显示的网格。从您的 CSS 中,您可以使用任何数字来引用网格线,或者您可以使用定义的名称,或者两者混合使用。因此,您可以说一个网格项从列线3延伸到线steve,从行线skylight延伸到线2

请注意,一个网格线可以有多个名称。您可以使用任何一个名称来引用给定的网格线,尽管您不能像多个类名那样将它们组合在一起。您可能会认为这意味着应该避免重复网格线名称,但并非总是如此,很快您就会看到原因。

image

图 12-6. 网格线编号和名称

我们在图 12-6 中故意使用了愚蠢的网格线命名,以说明您可以选择任何喜欢的名称,并避免暗示存在“默认”名称的可能性。如果您看到第一行使用了start,您可能会认为第一行总是这样命名的。错了。如果您想要将一个元素从start拉伸到end,您需要自己定义这些名称。幸运的是,这很简单。

正如我们所说,可以使用许多值模式来定义网格模板。我们将从更简单的开始,逐步过渡到更复杂的模式。

注意

在我们讨论subgrid值之前(在“使用子网格”一节中),我们需要先定义网格轨道和网格区域的命名、大小、组合等内容。

使用固定宽度网格轨道

作为我们的初始步骤,让我们创建一个其网格轨道宽度固定的网格。这里的固定宽度并不一定是像素或 em 单位的固定长度;百分比也可以在这里算作固定宽度。在这个语境下,固定宽度 意味着网格线的间距不会因为网格轨道内内容的变化而改变。

因此,例如,这被视为定义三个固定宽度网格列的示例:

#grid {display: grid;
    grid-template-columns: 200px 50% 100px;}

这将在距离网格容器起始位置(默认为左侧)200 像素处放置一条线;第二条网格线距第一条线的距离为网格容器宽度的一半;第三条线距第二条线 100 像素。这在图 12-7 中有示例。

image

图 12-7. 网格线放置

尽管第二列的大小可以随着网格容器的尺寸变化而改变,但它不会根据网格项的内容改变。无论放置在第二列中的内容是多宽或多窄,该列的宽度始终是网格容器宽度的一半。

最后一条网格线确实没有达到网格容器的右边缘。没关系;它不必如此。如果您希望它达到右边缘——您很可能会这样希望——稍后您将看到各种处理方法。

这一切都很好,但如果您想要为网格线命名怎么办?只需将任何您想要的网格线名称放在值中的适当位置,用方括号括起来即可。就是这样!让我们在前面的示例中添加一些名称,结果显示在图 12-8 中:

#grid {display: grid;
    grid-template-columns:
        [start col-a] 200px [col-b] 50% [col-c] 100px [stop end last];
    }

image

图 12-8. 网格线命名

令人满意的是,添加名称使每个值都清楚地指定了网格轨道的宽度,这意味着宽度值的两侧始终存在网格线。因此,对于我们拥有的三个宽度,实际上创建了四条网格线。

行网格线的放置方式与列完全相同,正如图 12-9 所示:

#grid {display: grid;
    grid-template-columns:
        [start col-a] 200px [col-b] 50% [col-c] 100px [stop end last];
    grid-template-rows:
        [start masthead] 3em [content] 80% [footer] 2em [stop end];
    }

image

图 12-9. 创建网格

我们有几件事情需要指出。首先,列和行线都有名称startend。这完全没问题。行和列不共享相同的命名空间,因此可以在这两个上下文中重复使用这样的名称。

第二个是content行轨道的百分比值。这是相对于网格容器的高度计算的;因此,一个高度为 500 像素的容器将产生一个高度为 400 像素的content行(因为此行的百分比值为80%)。通常情况下,这要求您预先知道网格容器的高度,但并非总是如此。

您可能会认为我们可以说100%并填充空间,但这并不起作用,如图 12-10 所示:content行轨道将与网格容器本身一样高,从而将footer行轨道推出容器外:

#grid {display: grid;
    grid-template-columns:
        [start col-a] 200px [col-b] 50% [col-c] 100px [stop end last];
    grid-template-rows:
        [start masthead] 3em [content] 100% [footer] 2em [stop end];
    }

image

图 12-10. 超过网格容器

处理这种情况的一种方式(不一定是最佳方式)是minmax行的值,告诉浏览器您希望行不低于一定值,也不高于另一定值,留给浏览器填写确切的值。这是使用minmax(a,b)模式完成的,其中a是最小尺寸,b是最大尺寸:

#grid {display: grid;
    grid-template-columns:
        [start col-a] 200px [col-b] 50% [col-c] 100px [stop end last];
    grid-template-rows:
        [start masthead] 3em [content] minmax(3em,100%) [footer] 2em [stop end];
    }

此代码表示content行的高度永远不应小于 3 个 em,并且永远不应比网格容器本身更高。这允许浏览器将其大小增加到足以适应从mastheadfooter轨道剩余空间中获得的大小,但不再增加。它还允许浏览器将其缩短,只要不短于3em,因此这并非保证结果。图 12-11 展示了此方法的一种可能结果。

image

图 12-11. 适应网格容器

类似地,带有相同警告的情况下,minmax()可以用来帮助col-b列充满网格容器中的空间。使用minmax()需要记住的是,如果max小于min,则会舍弃max值,并使用min值作为固定宽度轨道长度。因此,对于任何小于50px的字体大小值,minmax(100px, 2em)将解析为100px

如果minmax()的行为不确定使您感到不安,CSS 提供了这种情况的替代方案。我们也可以使用calc()值模式来计算轨道的高度(或宽度)。例如:

    grid-template-rows:
        [start masthead] 3em [content] calc(100%-5em) [footer] 2em [stop end];

这将产生一个content行,其高度正好等于网格容器减去mastheadfooter高度之和,正如我们在上一个图中看到的。

尽管这在某种程度上可行,但却是一种比较脆弱的解决方案,因为任何对mastheadfooter高度的更改也将需要调整计算。如果你希望多个网格轨道以这种方式灵活,那么这种方法就会变得更加困难(或不可能)。恰好,CSS 有更加强大的方法来处理这种情况,接下来你会看到。

使用灵活的网格轨道

到目前为止,我们所有的网格轨道都是不灵活的——它们的大小由长度度量或网格容器的尺寸确定,但不受其他任何考虑的影响。灵活的网格轨道则可以基于网格容器中未被不灵活轨道占用的空间量;或者,也可以基于整个网格轨道的实际内容。

分数单位

如果你想将可用空间按某个分数分配并将分数分配给各个列,fr单位就派上用场了。一个fr是一定量的灵活空间,代表网格中剩余空间的一部分。

在最简单的情况下,你可以将整个容器均匀分成若干份。例如,如果你想要四列,可以这样写:

grid-template-columns: 1fr 1fr 1fr 1fr;

在这个非常具体且有限的情况下,这相当于说以下内容:

grid-template-columns: 25% 25% 25% 25%;

图 12-12 展示了其中任何一个的结果。

图像

图 12-12。将容器分成四列

这是因为网格容器的所有空间都是“剩余空间”,因此所有这些空间都可以通过fr长度来分割。我们稍后会详细介绍非灵活网格轨道的情况。

回到前面的例子,假设我们想要添加第五列并重新分配列的大小,使它们仍然相等。如果我们使用百分比值,我们将不得不重写整个值为五个20%的实例。不过,使用fr,我们只需将值添加另一个1fr,所有工作都将自动完成:

grid-template-columns: 1fr 1fr 1fr 1fr 1fr;

fr单位的工作方式是将所有fr值相加,并将网格中所有剩余空间除以该总数。然后,每个轨道根据其fr值得到相应数量的这些分数。

在我们的第一个例子中,我们有四个1fr值,因此它们的1被加在一起得到总数 4。然后将可用空间除以 4,每列都得到其中的四分之一。当我们添加第五个1fr时,空间被分成 5 份,每列得到其中的五分之一。

您不必总是将1与您的fr单位一起使用!假设您想将空间分为三列,其中中间列的宽度是其他两列的两倍。代码将如下所示:

grid-template-columns: 1fr 2fr 1fr;

再次,这些值相加得到 4,然后我们将这个 4 除以 1(代表整体),所以在这种情况下基础的 fr0.25。因此,第一和第三条轨道的宽度为容器宽度的 25%,而中间列是容器宽度的一半,因为它是 2fr,即 0.25 的两倍,即 0.5,或 50%。

你并不仅限于整数。比如,苹果派的食谱卡片可以使用这些列布局:

grid-template-columns: 1fr 3.14159fr 1fr;

关于这一点的数学计算就留给你做练习了。(幸运的是!只需记住从1 + 3.14159 + 1开始,你就会有一个很好的起步。)

这是一种方便的方式来切割容器,但这里不仅仅是用百分比替换更直观的东西。分数单位在有些固定轨道和某些灵活空间时才能真正发挥作用。例如,考虑以下情况,如图 12-13 所示:

grid-template-columns: 15em 1fr 10%;

image

图 12-13. 给予中间列任何可用的内容

在这里,浏览器为第一和第三条轨道分配了它们的不可变宽度,然后将剩余的网格容器空间给了中间轨道。对于一个宽度为 1000 像素的网格容器,其font-size是通常的浏览器默认值16px,第一列将宽 240 像素,第三列将宽 100 像素。总共 340 像素,剩下 660 像素没有分配到固定轨道。分数单位总共为 1,因此 660 除以 1,得到 660 像素,全部分配给单个的 1fr 轨道。如果网格容器的宽度增加到 1400 像素,第三列将宽 140 像素,中间列将宽 1020 像素。

就像这样,我们有了一系列固定和灵活的列。我们可以继续这样做,将任何灵活的空间分割成任意数量的分数。考虑这个例子:

width: 100em; grid-template-columns: 15em 4.5fr 3fr 10%;

在这种情况下,列的大小如图 12-14 所示。

image

图 12-14. 灵活的列宽度

列的宽度从左到右依次为:15, 45, 30 和 10 em。第一列有固定宽度15em。最后一列是 100 em 的10%,即 10 em。这留下 75 em 用于灵活列的分配。两者加起来总共为 7.5 fr。对于较宽的列,4.5 ÷ 7.5 等于 0.6,乘以 75 em 等于 45 em。同样地,3 ÷ 7.5 = 0.4,乘以 75 em 等于 30 em。

是的,不可否认,我们在这个例子中进行了一些操作:fr 总数和 width 值被设计成能够为各个列产生漂亮的整数。这完全是为了帮助理解。如果你想用不那么整洁的数字来进行操作过程,请考虑在上一个例子中使用 92.5em1234px 作为 width 值。

如果你想为给定的轨道定义最小或最大大小,minmax()会非常有用。延续上一个示例,假设第三列无论如何都不应该少于5em宽。那么 CSS 会如下所示:

grid-template-columns: 15em 4.5fr minmax(5em,3fr) 10%;

现在布局将在中间有两列灵活列,直到第三列达到5em宽。在该点以下,布局将有三个不灵活的列(分别为15em5em10%宽),和一个灵活列,它会获得所有剩余的空间(如果有的话)。一旦运行了这个数学计算,发现在宽度达到30.5556em之前,网格将有一个灵活列。超过这个宽度,将会有两个灵活列。

你可能会认为这种方法会反过来—例如,如果你想使列轨道在某一点之前灵活,然后在之后变成固定列,你会声明一个最小的fr值。但很遗憾,这不会起作用,因为minmax()表达式的最小位置不允许fr单位。因此,任何作为最小值提供的fr值都会使整个声明无效。

说到设置为 0,让我们看一下显式设置为0的最小值,就像这样:

grid-template-columns: 15em 1fr minmax(0,500px) 10%;

图 12-15 展示了第三列可以保持 500 像素宽度的最窄网格宽度。再窄一点,minmax 的列将小于 500 像素。再宽一点,第二列,即fr列,会增长到超过零宽度,而第三列保持在 500 像素宽度。

图像

图 12-15. minmax列大小调整

如果你仔细观察,你会看到1fr标签位于15emminmax(0,500px)列之间的边界上。这是因为1fr被放置在其左边缘处于第二列网格线上,并且由于没有剩余空间可以弹性伸缩,它没有宽度。同样地,minmax被放置在第三列网格线上。只是在这种特定情况下,第二列和第三列的网格线在同一位置(这就是为什么1fr列宽度为零的原因)。

如果你遇到最小值大于最大值的情况,整个事情会被替换为最小值。因此,minmax(``500px,200px)会被视为简单的500px。你可能不会这么明显地这样做,但在混合百分比和分数时,这个特性是很有用的。因此,你可以有一个列是minmax(10%,1fr),它可以灵活地调整到弹性列小于网格容器大小的 10%时,它会停留在10%

分数单位和minmax同样可以在行上使用,就像在列上一样容易。只是行很少以这种方式进行大小设置。你可以轻松地想象设置一个布局,其中页眉和页脚是固定轨道,而内容可以在一定点下灵活调整。可能看起来像这样:

grid-template-rows: 3em minmax(5em,1fr) 2em;

这个方法可以工作,但更有可能的情况是,你会想要按内容的高度来调整行的大小,而不是网格容器高度的一部分。下一节将详细展示如何实现这一点。

内容感知的轨道

设置网格轨道占用可用空间的一部分,或者占用固定的空间是一回事。但是如果你想要对齐页面上的一堆部件,而无法保证它们的宽度或高度,这就是min-contentmax-content发挥作用的地方。(详见第 6 章关于这些关键词的详细解释。)

在 CSS Grid 中使用这些尺寸关键词的强大之处在于,它们适用于它们定义的整个网格轨道。例如,如果你将一列设置为max-content,整个列轨道将与其内最宽的内容一样宽。这最容易通过一个图像网格(此处为 12 张)来说明,网格声明如下,并在图 12-16 中展示:

#gallery {display: grid;
    grid-template-columns: max-content max-content max-content max-content;
    grid-template-rows: max-content max-content max-content;}

css5 1216

图 12-16. 按内容调整网格轨道大小

查看列,我们可以看到每个列轨道的宽度都与该轨道内最宽的图像一样。如果有一堆竖直图片排列在一起,列就会更窄;如果出现横向图片,列就会被扩展足够容纳。同样的情况也发生在行上。每行的高度与其内最高的图像一样高,所以如果一行内全是短图片,那行的高度也会较短。

这里的优势在于,无论内容是什么,这种方法都适用。比如我们给照片加上标题。所有的列和行都会根据需要调整大小,以处理文本和图片,如图 12-17 所示。

这不是一个成熟的设计——图片摆放错乱,而且没有尝试约束标题的宽度。事实上,这正是我们对于列宽使用max-content值所期望的。因为它意味着“使这一列足够宽以容纳其所有内容”,这就是我们得到的结果。

image

图 12-17. 围绕混合内容调整网格轨道大小

重要的是要意识到,即使网格轨道溢出网格容器,这种效果仍然存在。即使我们为网格容器分配了width: 250px这样的值,图片和标题的布局也会保持不变。这就是为什么像max-content这样的东西经常出现在minmax()语句中的原因。考虑以下示例,在这些示例中,使用和不使用minmax()的网格并排显示。在两种情况下,网格容器都有一个阴影背景(见图 12-18)。

#g1 {display: grid;
    grid-template-columns: max-content max-content max-content max-content;
    }
#g2 {display: grid;
    grid-template-columns: minmax(0,max-content) minmax(0,max-content)
          minmax(0,max-content) minmax(0,max-content);
    }

image

图 12-18. 使用和不使用minmax()调整网格轨道大小

在第一种情况下,网格项完全包含其内容,但它们溢出了网格容器。在第二种情况下,minmax() 指示浏览器将列保持在 0max-content 的范围内,因此如果可能的话,它们将全部适合网格容器中。一个变种是声明 minmax(min-content, max-content),这可能导致与 0, max-content 方法略有不同的结果。

在第二个示例中,一些图像溢出其单元格的原因是轨道已根据 minmax(0,max-content) 适合于网格容器。它们无法在每个轨道中达到 max-content,但它们可以尽可能接近,同时仍然适合网格容器。如果内容比轨道更宽,它们将突出并覆盖其他轨道。这是标准的网格行为。

如果您想知道如果您在列和行中都使用 min-content,那会发生什么,它几乎与仅将 min-content 应用于列并将行保持不变的情况相同。这是因为网格规范指示浏览器先解析列尺寸,然后再解析行尺寸。

您可以与网格跟踪尺寸一起使用的另一个关键字是 auto,它也恰好是任何网格跟踪宽度的默认值。作为最小值,它被视为网格项的最小尺寸,由 min-widthmin-height 定义。作为最大值,它与 max-content 一样对待。您可能认为这意味着它只能在 minmax() 语句中使用,但这并非如此。您可以随时使用它,它将在最小或最大角色中扮演一个角色。它承担的角色取决于其周围其他轨道值的方式,这些方式在这里讲述起来实际上太复杂了。与 CSS 的许多其他方面一样,使用 auto 本质上是让浏览器自己决定。有时候这很好,但一般来说,您可能会希望避免使用它。

注意

对于上述最后一种情况有一个警告:auto 值允许通过 align-contentjustify-content 属性调整网格项的大小,这是我们将在 “设置网格对齐” 中讨论的一个话题。由于 auto 值是唯一允许此操作的跟踪尺寸值,因此仍然可能有非常好的理由使用 auto

适合跟踪内容

除了 min-contentmax-content 关键字外,fit-content() 函数还允许您更紧凑地表示某些类型的尺寸模式。虽然有点复杂,但努力是值得的:

fit-content() 函数接受一个 <长度> 或 <百分比> 作为其参数,如下所示:

#grid  {display: grid; grid-template-columns: 1fr fit-content(150px) 2fr;}
#grid2 {display: grid; grid-template-columns: 2fr fit-content(50%) 1fr;}

在我们探讨这意味着什么之前,让我们考虑一下规范给出的伪公式:

  • fit-content(argument) => min(max-content, max(min-content, argument))

这基本上意味着,“找出哪个更大,min-content大小还是提供的参数,然后取这个结果,再从这个结果和max-content大小中选择较小的一个。”这可能会令人困惑!

我们觉得更好的表达方式是“fit-content(*argument*)等同于minmax(min-content,max-content),只是给定的参数设置了一个上限,类似于max-widthmax-height。”让我们考虑这个例子:

#example {display: grid; grid-template-columns: fit-content(50ch);}

这里的参数是50ch,或者说与 50 个零(0)字符并排的宽度相同。因此我们设置了一个单列,其内容适应这个尺寸。

对于初始情况,假设内容仅为 29 个字符长,测量为 29 ch(因为它是等宽字体)。这意味着max-content的值为29ch,列宽度将仅为此大小,因为它最小化到该尺寸——29ch50chmin-content的最大值要小。

现在假设添加了一堆文本内容,使得有 256 个字符,因此宽度为256ch(没有换行)。这意味着max-content的值为256ch。这远远超出了50ch的参数,因此列被限制为min-content50ch中较大的值,即50ch

作为进一步说明,请考虑以下结果,如 Figure 12-19 所示:

#thefollowing  {
    display: grid;
    grid-template-columns:
        fit-content(50ch) fit-content(50ch) fit-content(50ch);
    font-family: monospace;}

image

Figure 12-19. 使用 fit-content() 调整网格跟踪大小

注意第一列比其他两列更窄。它的29ch内容被最小化到该大小。其他两列的内容超过了50ch,因此它们被限制在50ch内换行。

现在让我们考虑一下,如果将图像添加到第二列会发生什么情况。我们将其设置为500px宽,这恰好比本例中的50ch更宽。对于该列,将确定min-content50ch的最大值。正如我们所说,较大的值是min-content,也就是500px(图像的宽度)。然后确定500pxmax-content最小值。文本作为单行呈现,会超过500px,因此最小值为500px。因此,第二列现在宽度为 500 像素。这在 Figure 12-20 中有所描述。

image

Figure 12-20. 适配宽内容

如果你比较 Figure 12-19 和 12-20,你会看到第二列文本在不同宽度下换行的不同点。同时比较第三列文本,它的换行点也不同。

由于第一列和第二列的大小已确定,第三列的空间略小于50chfit-content(50ch)函数仍然起作用,但在此处,它是在可用空间内进行的。请记住,50ch参数是一个上限,而不是固定尺寸。

这是fit-content()相对于不太灵活的minmax()的一大优势之一。当内容不多时,它允许您将轨道缩小到其最小的content-size,而在有大量内容时仍设置轨道尺寸的上限。

也许您曾经在之前的示例中对重复的网格模板值感到好奇,以及如果您需要超过三四个网格轨道会发生什么。您是否需要逐个编写每个轨道宽度?实际上不需要,接下来您将看到。

重复网格轨道

如果您想设置一堆相同尺寸的网格轨道,您可能不想逐个输入它们。幸运的是,repeat()可以确保您无需这样做。

假设我们想要设置每 5 ems 一个列网格线,有 10 列轨道。这里是如何做到的:

#grid {display: grid;
    grid-template-columns: repeat(10, 5em);}

就这样。完成。十个列轨道,每个轨道宽度为5em,总共 50 ems 的列轨道。这肯定比逐次键入5em 10 次要方便!

任何轨道大小的值都可以在重复中使用,从min-contentmax-contentfr值再到auto等等,您可以组合多个尺寸值。假设我们想要定义一个列结构,有一个2em的轨道,然后是一个1fr的轨道,然后是另一个1fr的轨道,而且我们希望重复这种模式三次。这里是如何做到的,结果显示在图 12-21 中:

#grid {display: grid;
    grid-template-columns: repeat(3, 2em 1fr 1fr);}

图片

图 12-21. 重复轨道模式

请注意,最后一列轨道是一个1fr轨道,而第一列轨道是2em宽。这是repeat()写法的效果。如果要在repeat()表达式后添加一个2em轨道以平衡情况,这样做很容易:

#grid {display: grid;
    grid-template-columns: repeat(3, 2em 1fr 1fr) 2em;}

这突显了repeat可以与任何其他轨道大小值(甚至其他重复)结合在一起构建网格的事实。您唯一不能做的是在另一个repeat内部嵌套一个repeat

除此之外,在repeat()值中几乎可以放置任何东西。这里有一个直接来自网格规范的例子:

#grid {
    display: grid;
    grid-template-columns: repeat(4, 10px [col-start] 250px [col-end]) 10px;}

在这种情况下,有四个重复的 10 像素轨道,一个命名的网格线,一个 250 像素轨道,然后另一个命名的网格线。然后,在四次重复之后,还有一个最后的 10 像素列轨道。是的,这意味着将有四个名为col-start的列网格线和另外四个名为col-end的网格线,如图 12-22 所示。这是可以接受的;网格线名称不需要唯一。

图片

图 12-22. 带有命名网格线的重复列

有一件事需要记住,如果你要重复命名线条,那么如果将两个命名线条放在一起,它们将合并成一个双重命名的网格线。换句话说,以下两个声明是等效的:

grid-template-rows: repeat(3, [top] 5em [bottom]);
grid-template-rows: [top] 5em [bottom top] 5em [top bottom] 5em [bottom];
注意

如果你担心将同一个名称应用于多个网格线,不用担心:没有任何阻止,甚至在某些情况下还可能有所帮助。我们将在 “使用列和行线” 中探讨处理此类情况的方法。

自动填充轨道

CSS 提供了一种设置简单模式并重复直到填满网格容器的方法。这不像常规的 repeat() 那样复杂——至少目前不是——但仍然非常实用。

例如,假设我们想要先前的行模式在网格容器舒适地接受的情况下重复多次:

grid-template-rows: repeat(auto-fill, [top] 5em [bottom]);

这将定义每 5 ems 一个行线,直到没有更多空间为止。因此,对于一个高度为 11 ems 的网格容器,以下是等效的:

grid-template-rows: [top] 5em [bottom top] 5em [bottom];

如果网格容器的高度增加到超过 15 ems,但小于 20 ems,则以下是等效声明:

grid-template-rows: [top] 5em [bottom top] 5em [top bottom] 5em [bottom];

见 图 12-23 关于三个网格容器高度下自动填充行的示例。

image

图 12-23. 三种高度下的自动填充行

自动重复的一个限制是它只能采用可选的网格线名称、固定轨道大小和另一个可选的网格线名称。因此,[top] 5em [bottom] 表示大致的最大值模式。你可以省略命名线并只重复 5em,或者只省略一个名称。

不可能自动重复多个固定轨道大小,也不能自动重复灵活的轨道大小。同样地,你不能在自动重复的模式中使用内在的轨道大小,因此像 min-contentmax-content 这样的值不能放入自动重复的模式中。

注意

你可能希望能够自动重复多个轨道大小,以定义内容列周围的间距。通常这是不必要的,因为属性 row-gapcolumn-gap 及其简写 gap 已在 第十一章 中介绍,但它们同样适用于 CSS Grid。

此外,在给定的轨道模板中只能有一个自动重复。因此,以下情况将被允许:

grid-template-columns: repeat(auto-fill, 4em) repeat(auto-fill, 100px);

然而,你可以将固定重复的轨道与自动填充的轨道结合起来。例如,你可以从三个宽列开始,然后用窄轨道填充网格容器的其余部分(假设有空间)。这将看起来像这样:

grid-template-columns: repeat(3, 20em) repeat(auto-fill, 2em);

你也可以反过来:

grid-template-columns: repeat(auto-fill, 2em) repeat(3, 20em);

这是因为网格布局算法首先为固定轨道分配空间,然后用自动重复轨道填充剩余空间。最终的结果是有一个或多个自动填充的 2 em 轨道,然后是三个 20 em 轨道。图 12-24 展示了两个示例。

image

图 12-24。自动填充列与固定列旁边

使用auto-fill时,即使由于某些原因无法适应网格容器,你仍然会得到至少一个重复的跟踪模板。即使有些跟踪没有内容,你也会得到尽可能多的跟踪。例如,假设你设置了一个自动填充,放置了五列,但实际上只有前三列有网格项。其余两列将保持原位,保持布局空间的开放状态。

另一方面,如果使用auto-fit,那些不包含任何网格项的跟踪将被压缩到零宽度,尽管它们(及其相关的网格线)仍然是网格的一部分。否则,auto-fit的行为与auto-fill相同。假设以下情况:

grid-template-columns: repeat(auto-fit, 20em);

如果网格容器有足够的空间容纳五个列跟踪(即宽度超过 100 ems),但有两个跟踪没有任何网格项可放入,那些空的网格跟踪将被丢弃,留下三个包含网格项的列跟踪。剩余的空间根据align-contentjustify-content的值处理(参见“设置网格对齐”中讨论)。图 12-25 中展示了auto-fillauto-fit的简单比较,其中彩色框中的数字表示它们所附加到的网格列编号。

image

图 12-25。使用auto-fillauto-fit

定义网格区域

或许你更愿意“画出你的网格图”,因为这样做既有趣又可以作为自解释的代码。事实证明,你可以使用grid-template-areas属性几乎完全做到这一点。

我们可以详细描述它的工作原理,但展示它会更有趣。以下规则的结果显示在图 12-26 中:

#grid {display: grid;
    grid-template-areas:
        "h h h h"
        "l c c r"
        "l f f f";}

image

图 12-26。一个简单的网格区域设置

没错:字符串值中的字母用于定义网格区域的形状。是真的!而且你甚至不限于单个字母!例如,我们可以像这样扩展前面的例子:

#grid {display: grid;
    grid-template-areas:
        "header     header    header    header"
        "leftside   content   content   rightside"
        "leftside   footer    footer    footer";}

网格布局与图 12-26 中显示的相同,尽管每个区域的名称可能会不同(例如,footer而不是f)。

在定义模板区域时,空白会被折叠,因此你可以使用它(如前面的例子所示),在grid-template-areas的值中将列名对齐视觉上的线条。你可以使用空格或制表符来对齐名称,无论哪种方式都会使你的同事感到不适。或者你可以只用一个空格来分隔每个标识符,不必担心名称之间的对齐问题。甚至在字符串之间不换行也可以,以下版本与美观打印版本一样有效:

grid-template-areas: "h h h h" "l c c r" "l f f f";

你不能将这些独立的字符串合并成一个字符串并具有相同的含义。每个新字符串(由引号分隔)定义网格中的新行。因此,前面的例子和之前的例子一样,定义了三行。假设我们将它们全部合并成一个字符串,如下所示:

grid-template-areas:
    "h h h h
 l c c r
 l f f f";

接着我们会有一行包含 12 列,从四列区域h开始,到三列区域f结束。换行只是作为空格分隔一个标识符与另一个的方式,并没有其他意义。

如果仔细查看这些值,您可能会意识到每个单独的标识符代表一个网格单元。让我们回顾一下本节中的第一个例子,并考虑图 12-27 中显示的结果,它使用 Firefox 的 Grid Inspector 标记每个单元格:

#grid {display: grid;
    grid-template-areas:
        "h h h h"
        "l c c r"
        "l f f f";}

图片

图 12-27. 带有其网格区域标识符的网格单元格

这与图 12-26 中的布局结果完全相同,但这里我们展示了grid-template-areas值中每个网格标识符如何对应一个网格单元。一旦所有单元格被标识,浏览器将合并任何具有相同名称的相邻单元格为一个包含它们所有的区域,只要它们描述的是矩形形状!如果尝试设置更复杂的区域,则整个模板将无效。因此,以下情况将导致未定义任何网格区域:

#grid {display: grid;
    grid-template-areas:
        "h h h h"
        "l c c r"
        "l l f f";}

看看l是如何勾勒出一个L形状?这个微小的改变导致整个grid-template-areas值作废。Grid 布局的未来版本可能允许非矩形形状,但目前存在此限制。

如果您只想定义一些网格单元格作为网格区域的一部分,而将其他单元格保留未标记,则可以使用一个或多个.字符来填充这些未命名的单元格。假设您只想定义一些标题、页脚和侧边栏区域,并留下其他未命名的区域。那看起来会像这样,结果显示在图 12-28 中:

#grid {display: grid;
    grid-template-areas:
        "header  header  header  header"
        "left    ...     ...     right"
        "footer  footer  footer  footer";}

图片

图 12-28. 带有一些未命名网格单元格的网格

网格中心的两个单元格不属于命名区域,它们在模板中用空单元格标记.标识符)表示。每当出现...序列时,我们都可以使用一个或多个空标记——所以left . . rightleft ... ... right同样适用。

你可以简单或者创意地命名你的单元格,想把头部称为ronaldo,尾部称为podiatrist,随你喜欢。你甚至可以使用大于代码点 U+0080 的任何 Unicode 字符,所以ConHugeCo©®™åwësømë都是完全有效的区域标识符……包括表情符号! 现在,为了调整这些区域创建的网格轨道的大小,我们要引入我们的老朋友grid-template-columnsgrid-template-rows。让我们把它们添加到前面的例子中,并显示结果如图 12-29 所示:

#grid {display: grid;
    grid-template-areas:
        "header  header  header  header"
        "left    ...     ...     right"
        "footer  footer  footer  footer";
    grid-template-columns: 1fr 20em 20em 1fr;
    grid-template-rows: 40px 10em 3em;}

image

图 12-29. 命名区域和尺寸轨道

因此,通过命名网格区域创建的列和行都被赋予了轨道尺寸。如果我们给出的轨道尺寸超过了区域轨道的数量,那么将会在命名区域之外添加更多的轨道。因此,以下 CSS 将导致图 12-30 所示的结果:

#grid {display: grid;
    grid-template-areas:
        "header  header  header  header"
        "left    ...     ...     right"
        "footer  footer  footer  footer";
    grid-template-columns: 1fr 20em 20em 1fr 1fr;
    grid-template-rows: 40px 10em 3em 20px;}

image

图 12-30. 在命名区域之外添加更多轨道

那么,既然我们正在命名区域,为什么不混合一些命名网格线呢?正如事情所发生的那样,我们已经做到了:为header区域命名会自动在其第一列网格线第一行网格线上添加header-start名称,并且在其第二列和第二行网格线上添加header-end名称。对于footer区域,footer-startfooter-end名称也会自动分配到其网格线上。

网格线延伸到整个网格区域,因此许多这些名称是重合的。图 12-31 展示了由以下模板创建的线条的命名:

    grid-template-areas:
        "header    header    header    header"
        "left      ...       ...       right"
        "footer    footer    footer    footer";

image

图 12-31. 将隐式网格线名称显示为显式

现在让我们通过在我们的 CSS 中添加一些显式网格线名称来更加混合一些。根据以下规则,网格中的第一列网格线将添加名称begin,网格中的第二行网格线将添加名称content

#grid {display: grid;
    grid-template-areas:
        "header  header  header  header"
        "left    ...     ...     right"
        "footer  footer  footer  footer";
    grid-template-columns: [begin] 1fr 20em 20em 1fr 1fr;
    grid-template-rows: 40px [content] 1fr 3em 20px;}

再次强调:这些网格线名称添加到由命名区域创建的隐式网格线名称中。网格线名称永远不会取代其他网格线名称,而只会不断堆叠。

更加有趣的是,这种隐式命名机制也可以反过来使用。假设你根本不使用grid-template-areas,而是设置了一些像这样命名的网格线,正如图 12-32 所示:

    grid-template-columns:
         [header-start footer-start] 1fr
         [content-start] 1fr [content-end] 1fr
         [header-end footer-end];
    grid-template-rows:
        [header-start] 3em
        [header-end content-start] 1fr
        [content-end footer-start] 3em
        [footer-end];

image

图 12-32. 将隐式网格区域名称显示为显式

因为网格线使用name-start/name-end的形式,它们定义的网格区域是隐式命名的。坦率地说,这比另一种方式更加笨拙,但是如果你需要的话,这种能力是存在的。

请记住,为了创建一个命名的网格区域,你不需要所有四个网格线都被命名,尽管你可能确实需要它们全部被命名,以便在你希望的位置创建一个命名的网格区域。考虑以下示例:

    grid-template-columns: 1fr [content-start] 1fr [content-end] 1fr;
    grid-template-rows: 3em 1fr 3em;

这仍然会创建一个名为content的网格区域。只是这个命名区域将放置在所有定义的行之后的新行中。奇怪的是,在定义的行之后但在包含content的行之前会出现一个额外的空行。这已确认是预期行为。因此,如果你尝试通过命名网格线来创建一个命名区域,并且错过了一个或多个网格线,你的命名区域将有效地悬挂在网格的一侧,而不是成为整体网格结构的一部分。

所以,再次强调,如果你想创建命名的网格区域,最好明确命名网格区域,并让start-end-网格线名称隐式创建,而不是反过来。

将元素放置在网格中

信不信由你,我们到现在为止都没有讨论过网格项在网格中实际放置的方式。

使用列和行线

根据你是否想引用网格线还是网格区域,放置网格项有几种方法。我们将从四个简单的属性开始,这些属性将元素附加到网格线上。

这些属性让你可以说:“我希望元素的边缘连接到这个网格线。”正如 CSS Grid 的许多内容一样,展示起来比描述起来要容易得多,请考虑以下样式及其结果(参见图 12-33):

.grid {display: grid; width: 50em;
    grid-template-rows: repeat(5, 5em);
    grid-template-columns: repeat(10, 5em);}
.one {
    grid-row-start: 2; grid-row-end: 4;
    grid-column-start: 2; grid-column-end: 4;}
.two {
    grid-row-start: 1; grid-row-end: 3;
    grid-column-start: 5; grid-column-end: 10;}
.three {
    grid-row-start: 4;
    grid-column-start: 6;}

图片

图 12-33. 将元素附加到网格线

在这里,我们使用网格线号来说明元素在网格中应该如何放置。列号从左到右计数,行号从上到下计数。如果省略了结束的网格线,就像.three的情况一样,那么序列中的下一个网格线将用作结束线。

因此,在前面示例中的.three规则与此完全相同:

.three {
    grid-row-start: 4; grid-row-end: 5;
    grid-column-start: 6; grid-column-end: 7;}

实际上,还有另一种表达方式:你可以用span 1替换结束值,甚至只用span,像这样:

.three {
    grid-row-start: 4; grid-row-end: span 1;
    grid-column-start: 6; grid-column-end: span;}

如果你使用一个数字来设置span,你是在说:“横跨这么多网格轨道。”因此,我们可以像这样重写我们的早期示例,并得到完全相同的结果:

#grid {display: grid;
    grid-template-rows: repeat(5, 5em);
    grid-template-columns: repeat(10, 5em);}
.one {
    grid-row-start: 2; grid-row-end: span 2;
    grid-column-start: 2; grid-column-end: span 2;}
.two {
    grid-row-start: 1; grid-row-end: span 2;
    grid-column-start: 5; grid-column-end: span 5;}
.three {
    grid-row-start: 4; grid-row-end: span 1;
    grid-column-start: 6; grid-column-end: span;}

如果你在span中省略一个数字,它将被设置为1。你不能使用 0 或负数来设置span,只能使用正整数。

span的一个有趣特性是,您可以同时用于结束和开始网格线。span的精确行为是在网格线开始的方向上“远离”网格线计数。换句话说,如果您定义一个起始网格线并将结束网格线设置为span值,它将向网格末端搜索。相反,如果您定义一个结束网格线并使起始线为span值,则它将向网格起始端搜索。

这意味着以下规则将显示为 图 12-34 所示的结果(为了清晰起见,添加了列和行号):

#grid {display: grid;
    grid-rows: repeat(4, 2em); grid-columns: repeat(5, 5em);}
.box1 {grid-row: 1; grid-column-start: 3;      grid-column-end: span 2;}
.box2 {grid-row: 2; grid-column-start: span 2; grid-column-end: 3;}
.box3 {grid-row: 3; grid-column-start: 1;      grid-column-end: span 5;}
.box4 {grid-row: 4; grid-column-start: span 1; grid-column-end: 5;}

image

图 12-34. 跨越网格线

span编号相比,实际网格线值并不限于正整数。负数将从显式定义的网格线末尾向前计数。因此,要将元素放置到定义的网格的右下网格单元中,无论它可能有多少列或行,您只需说这个:

grid-column-start: -1;
grid-row-start: -1;

请注意,这不适用于任何隐式网格轨道,这是我们稍后会讨论的一个概念,只适用于您通过其中一个grid-template-*属性显式定义的网格线(例如,grid-template-rows)。

我们并非局限于网格线号码。如果有命名网格线,我们可以引用这些网格线,而不是(或与)号码一起使用。如果您有多个网格线名称的实例,可以使用数字来标识您正在谈论的网格线名称的哪个实例。因此,要从名为mast-slice的行网格的第四个实例开始,您可以说mast-slice 4。请查看下面的内容,如 图 12-35 所示,了解其工作原理的一些想法:

#grid {display: grid;
    grid-template-rows: repeat(5, [R] 4em);
    grid-template-columns: 2em repeat(5, [col-A] 5em [col-B] 5em) 2em;}
.one {
    grid-row-start: R 2;       grid-row-end: 5;
    grid-column-start: col-B;  grid-column-end: span 2;}
.two {
    grid-row-start: R;           grid-row-end: span R 2;
    grid-column-start: col-A 3;  grid-column-end: span 2 col-A;}
.three {
    grid-row-start: 9;
    grid-column-start: col-A -2;}

image

图 12-35. 将元素附加到命名网格线

注意当我们添加名称时span如何变化:指定span 2 col-A会导致网格项从其起始点(第三个col-A)跨越另一个col-A,并在其后的col-A结束。这意味着网格项实际上跨越四个列轨道,因为col-A出现在每隔一个列网格线上。

再次强调,负数从序列末尾向前计数,因此col-A -2获取了名为col-A的倒数第二个网格线实例。因为.three未声明结束线值,它们都设置为span 1。这意味着以下内容与前面示例中的.three完全相同:

.three {
    grid-row-start: 9; grid-row-end: span 1;
    grid-column-start: col-A -2; grid-row-end: span 1;}

使用带有命名网格线的名称的另一种方法——具体来说,是由网格区域隐式创建的命名网格线。例如,请考虑以下样式,如 图 12-36 所示:

grid-template-areas:
    "header     header    header    header"
    "leftside   content   content   rightside"
    "leftside   footer    footer    footer";
#masthead {grid-row-start: header;
	grid-column-start: header; grid-row-end: header;}
#sidebar {grid-row-start: 2; grid-row-end: 4;
	grid-column-start: leftside / span 1;}
#main {grid-row-start: content; grid-row-end: content;
	grid-column-start: content;}
#navbar {grid-row-start: rightside; grd-row-end: 3;
	grid-column-start: rightside;}
#footer {grid-row-start: 3; grid-row-end: span 1;
	grid-column-start: footer; grid-row-end: footer;}

image

图 12-36. 将元素附加到命名网格线的另一种方法

如果您提供自定义标识符(即您定义的名称),浏览器会查找具有该名称加上-start-end的网格线,具体取决于您是分配起始线还是结束线。因此,以下两种方式是等效的:

grid-column-start: header;        grid-column-end: header;
grid-column-start: header-start;  grid-column-end: header-end;

这有效是因为,正如我们在grid-template-areas中提到的,显式创建网格区域隐式地创建了周围的命名-start-end网格线。

最后一个可能值,auto,是相当有趣的。根据网格布局规范,如果网格线的起始/结束属性之一设置为auto,那表示“自动放置,自动跨度,或默认跨度为一”。在实践中,这通常意味着选择的网格线由网格流控制,这是我们尚未涵盖但即将涵盖的概念!对于起始线,auto通常意味着使用下一个可用的列或行线。对于结束线,auto通常意味着一个单元格的跨度。在这两种情况下,“通常”这个词是有意使用的:像任何自动机制一样,没有绝对的规则。

使用行和列的快捷方式

两个快捷属性允许您更紧凑地将元素附加到网格线。

这些属性的主要优点是它们使得声明用于布局网格项的起始和结束网格线变得更加简单。例如:

#grid {display: grid;
    grid-template-rows: repeat(10, [R] 1.5em);
    grid-template-columns: 2em repeat(5, [col-A] 5em [col-B] 5em) 2em;}
.one {
    grid-row: R 3 / 7;
    grid-column: col-B / span 2;}
.two {
    grid-row: R / span R 2;
    grid-column: col-A 3 / span 2 col-A;}
.three {
    grid-row: 9;
    grid-column: col-A -2;}

老实说,这比在单独的属性中每个起始和结束值更易读。除了更紧凑外,这些属性的行为基本上是您所期望的。如果您有两个由斜杠(/)分隔的部分,第一部分定义了起始网格线,第二部分定义了结束网格线。

如果只有一个值没有斜杠,它定义了起始网格线。结束网格线取决于你对起始线的设置。如果你为起始网格线提供一个名称,结束网格线也使用相同的名称。如果只提供一个数字,则第二个数字(结束线)设置为auto。这意味着以下两组是等效的:

grid-row: 2;
grid-row: 2 / auto;

grid-column: header;
grid-column: header / header;

在处理grid-rowgrid-column中网格线名称时内置的微妙行为涉及隐式命名的网格线。正如您可能记得的那样,定义一个命名网格区域会创建-start-end网格线。也就是说,给定一个名称为footer的网格区域,会隐式创建footer-start网格线在其顶部和左侧,以及footer-end网格线在其底部和右侧。

在这种情况下,如果你通过区域名称引用这些网格线,元素仍将正确放置。因此,以下样式的结果如图 12-37 所示:

#grid {display: grid;
    grid-template-areas:
        "header header"
        "sidebar content"
        "footer footer";
     grid-template-rows: auto 1fr auto;
     grid-template-columns: 25% 75%;}
#header {grid-row: header / header; grid-column: header;}
#footer {grid-row: footer; grid-column: footer-start / footer-end;}

图片

图 12-37. 通过网格区域名称附加到隐式网格线

您始终可以明确引用隐式命名的网格线,但如果只引用网格区域的名称,事情仍能正常进行。如果引用一个与网格区域不对应的网格线名称,它将回退到之前讨论的行为。详细来说,它与line-name 1的说法是一样的,所以下面两者是等价的:

grid-column: jane / doe;
grid-column: jane 1 / doe 1;

这就是为什么命名网格线与网格区域相同是有风险的。考虑以下情况:

    grid-template-areas:
        "header header"
        "sidebar content"
        "footer footer"
        "legal legal";
    grid-template-rows: auto 1fr [footer] auto [footer];
    grid-template-columns: 25% 75%;

这明确设置了位于footer行上方和“legal”行下方的名为footer的网格线…现在前面有麻烦了。假设我们添加了这个:

#footer {grid-column: footer; grid-row: footer;}

对于列线来说,没有问题。名称footer被扩展为footer / footer。浏览器查找具有该名称的网格区域并找到它,因此将footer / footer翻译为footer-start / footer-end#footer元素附加到这些隐式网格线上。

对于grid-row,一切都是从相同的起点开始的。名称footer变成了footer / footer,这被翻译为footer-start / footer-end。但这意味着#footer只会与footer行一样高。它不会延伸到下面的第二个明确命名的footer网格线,因为footerfooter-end的翻译(由于网格线名称与网格区域名称的匹配)具有优先权。

这一切的要点是:通常不建议为网格区域和网格线使用相同的名称。在某些情况下,您可能能够摆脱这种情况,但通常最好保持线和区域名称不同,以避免命名解析冲突。

使用隐式网格

到目前为止,我们仅关注了明确定义的网格:我们讨论了通过像grid-template-columns这样的属性定义的行和列轨道,以及如何将网格项附加到这些轨道中的单元格。

但是,如果我们尝试放置一个网格项,甚至只是网格项的一部分,超出明确创建的网格会发生什么?例如,考虑以下网格:

#grid {display: grid;
    grid-template-rows: 2em 2em;
    grid-template-columns: repeat(6, 4em);}

两行,六列。很简单。但假设我们定义一个网格项,它位于第一列,并从第一行网格线延伸到第四行:

.box1 {grid-column: 1; grid-row: 1 / 4;}

现在怎么办?我们只有由三条网格线界定的两行,并告诉浏览器超出这些范围,从第一行到第四行。

发生的是,会创建另一行网格线来处理这种情况。这条网格线及其创建的新行轨道都属于隐式网格。以下是创建隐式网格线(和轨道)以及它们如何布局的几个示例(参见图 12-38)。

.box1 {grid-column: 1; grid-row: 1 / 4;}
.box2 {grid-column: 2; grid-row: 3 / span 2;}
.box3 {grid-column: 3; grid-row: span 2 / 3;}
.box4 {grid-column: 4; grid-row: span 2 / 5;}
.box5 {grid-column: 5; grid-row: span 4 / 5;}
.box6 {grid-column: 6; grid-row: -1 / span 3;}
.box7 {grid-column: 7; grid-row: span 3 / -1;}

image

图 12-38。创建隐式网格线和轨道

那里发生了很多事情,让我们分解一下。首先,填充在各种编号框后面的盒子代表明确的网格;所有虚线表示隐式网格。

那些编号盒子呢?第一个box1在显式网格结束后添加了一个额外的网格行线。第二个box2从显式网格的最后一行线开始,跨越两个网格行线,因此添加了另一个隐式网格行线。第三个box3在显式网格的最后一行(第 3 行)结束,并且向后跨越两行,因此从显式网格的第一行开始。

对于box4,情况变得真正有趣。它结束于第五行线,也就是第二个隐式网格行线。它向后跨越三个网格行线,然而,它仍然从box3所在的同一行线开始。这是因为网格轨道跨度必须从显式网格内部开始计数。一旦开始,它们可以继续进入隐式网格(就像box2发生的那样),但不能从隐式网格内部开始计数。

因此,box4结束于第 5 行线,但其跨度从第 3 行线开始向后计数两行(span 2),直到达到第 1 行线。类似地,box5结束于第 5 行线,并向后跨越四行,这意味着它从第 -2 行线开始。记住:跨度计数必须从显式网格开始。它不必在那里结束。

在这之后,box6从显式网格的最后一行线(第 3 行)开始,并延伸到第六行线,又添加了一个隐式网格行线。放置在这里的目的是展示负数网格线引用是相对于显式网格的,并且从其末端向前计数。它们不指涉放置在显式网格开始之前的负数索引的隐式线。

如果你想在显式网格开始之前的隐式网格线上放置一个元素,box7展示了如何做到这一点:将其结束线放置在显式网格的某处,并向前跨过显式网格的开始。也许你已经注意到:box7占据了一个隐式列轨道。原始网格设置为创建六列,这意味着七个列线,第七个是显式网格的结束。当给box7设置grid-column: 7时,这相当于grid-column: 7 / span 1(因为缺少的结束线总是被假定为span 1)。这需要创建一个隐式列线,以便将网格项放置在隐式的第七列中。

现在让我们把这些原则和命名网格线结合起来。考虑下面的情况,如图 12-39 所示:

#grid {display: grid;
    grid-template-rows: [begin] 2em [middle] 2em [end];
    grid-template-columns: repeat(5, 5em);}
.box1 {grid-column: 1; grid-row: 2 / span end 2;}
.box2 {grid-column: 2; grid-row: 2 / span final;}
.box3 {grid-column: 3; grid-row: 1 / span 3 middle;}
.box4 {grid-column: 4; grid-row: span begin 2 / end;}
.box5 {grid-column: 5; grid-row: span 2 middle / begin;}

在这些示例中,你可以看到隐式网格中网格线名称的作用:每个隐式创建的线都具有被追寻的名称。以box2为例,它被赋予一个名为final的结束线,但实际上没有这样的线。因此,搜索跨度到显式网格的末端,并且在未找到所寻找的名称时创建了一个新的网格线,将其命名为final。(在图 12-39 中,隐式创建的线条名称是斜体且稍稍淡化了。)

image

图 12-39. 命名的隐式网格线和轨道

类似地,box3 从第一个显式行线开始,然后需要跨越三个 middle 命名线。它向前搜索并找到一个,然后继续寻找另外两个。找不到任何线后,它将名称 middle 附加到第一个隐式行线,然后对第二个隐式行线执行相同操作。因此,它跨越了显式网格的结束点两个隐式行线。

box4box5 发生的情况相同,只是从端点向后工作。你可以看到 box4end 行线(第 3 行线)结束,然后跨越到它能找到的第二个 begin 行线。这导致在第一个行线之前创建了一个隐式行线,名称为 begin。最后,box5begin(明确标记的 begin)向后跨越到它能找到的第二个 middle。由于找不到任何线,它将两个隐式行线标记为 middle,并在距离开始搜索位置最远的一个结束。

处理错误

我们需要涵盖几种情况,因为它们都属于“当事情变得一团糟时网格如何行事”的大框架。首先,如果你不小心将起始线放在结束线之后会怎么样?比如,像这样的情况:

grid-row-start: 5;
grid-row-end: 2;

所有发生的可能就是最初意图的实现:值被交换。因此,你最终得到以下结果:

grid-row-start: 2;
grid-row-end: 5;

第二,如果起始线和结束线都声明为某种跨度呢?例如:

grid-column-start: span;
grid-column-end: span 3;

如果这种情况发生,结束值被丢弃并替换为 auto。这意味着你最终会得到这样的结果:

grid-column-start: span;  /* 'span' is equal to 'span 1' */
grid-column-end: auto;

这会导致网格项的结束边根据当前网格流(我们很快会探讨这个主题)自动放置,并且起始边会提前一个网格线的位置。

第三,如果唯一指导网格项放置的是一个命名的跨度呢?换句话说,你会有这样一个情况:

grid-row-start: span footer;
grid-row-end: auto;

这是不允许的,因此在这种情况下 span footer 被替换为 span 1

使用区域

按行线和列线连接很好,但是如果你可以用一个属性引用网格区域会怎样?瞧:grid-area

让我们从一个简单使用 grid-area 开始:将一个元素分配到先前定义的网格区域。为此,我们将重新使用我们的老朋友 grid-template-areas,与 grid-area 和一些标记组合在一起,看看会有什么魔法结果(如 图 12-40 所示):

#grid {display: grid;
    grid-template-rows: 200px 1fr 3em;
    grid-template-columns: 20em 1fr 1fr 10em;
    grid-template-areas:
        "header     header    header    header"
        "leftside   content   content   rightside"
        "leftside   footer    footer    footer";}
#masthead {grid-area: header;}
#sidebar {grid-area: leftside;}
#main {grid-area: content;}
#navbar {grid-area: rightside;}
#footer {grid-area: footer;}

<div id="grid">
    <div id="masthead">…</div>
    <div id="main">…</div>
    <div id="navbar">…</div>
    <div id="sidebar">…</div>
    <div id="footer">…</div>
</div>

image

图 12-40. 将元素分配到网格区域

这就是所有的事情了:设置一些命名的网格区域来定义你的布局,然后用 grid-area 将网格项放入其中。如此简单而又强大。

另一种使用 grid-area 的方式是引用网格线而不是网格区域。公平警告:一开始可能会感到困惑。

这是一个定义了一些网格线和一些引用这些线的 grid-area 规则的网格模板示例,如 图 12-41 所示:

#grid {display: grid;
    grid-template-rows:
        [r1-start] 1fr [r1-end r2-start] 2fr [r2-end];
    grid-template-columns:
        [col-start] 1fr [col-end main-start] 1fr [main-end];}
.box01 {grid-area: r1 / main / r1 / main;}
.box02 {grid-area: r2-start / col-start / r2-end / main-end;}
.box03 {grid-area: 1 / 1 / 2 / 2;}

image

图 12-41。将元素分配到网格线

这些元素按照指示进行了放置。请注意网格线值的排序顺序。它们按照row-startcolumn-startrow-endcolumn-end的顺序列出。如果您在脑海中进行图解,您很快就会意识到这些值是逆时针(也称为逆时针)绕过网格项的,与我们从边距、填充、边框等方面熟悉的 TRBL 模式完全相反。此外,这意味着列和行引用并未分组在一起,而是分开处理。

如果您提供少于四个值,则缺失的值将从您提供的值中获取。如果您只使用三个值,则缺失的grid-column-endgrid-column-start相同(如果它是一个名称);如果起始行是一个数字,则结束行设置为auto。如果只提供两个值,则缺失的grid-row-endgrid-row-start复制(如果它是一个名称);否则,它被设置为auto

从此,您可能已经猜到如果只提供一个值会发生什么:如果它是一个名称,则将其用于所有四个值;如果它是一个数字,则其余的值设置为auto

这种一对四的复制模式实际上是将单个网格区域名称转换为使网格项填充该区域的方法。以下是等效的:

grid-area: footer;
grid-area: footer / footer / footer / footer;

现在回想一下上一节讨论的grid-columngrid-row的行为:如果网格线的名称与网格区域的名称匹配,它会根据需要转换为-start-end变体。这意味着前面的例子被翻译为以下内容:

grid-area: footer-start / footer-start / footer-end / footer-end;

这就是单个网格区域名称导致元素放置到相应网格区域的方式。

理解网格项重叠

迄今为止,在我们的网格布局中,我们非常小心地避免重叠。就像定位一样,绝对(懂了吗?)可以使网格项彼此重叠。让我们来看一个简单的例子,如图 12-42 所示:

#grid {display: grid;
    grid-template-rows: 50% 50%;
    grid-template-columns: 50% 50%;}
.box01 {grid-area: 1 / 1 / 2 / 3;}
.box02 {grid-area: 1 / 2 / 3 / 2;}

image

图 12-42。重叠的网格项

多亏了上述 CSS 中提供的网格数字,两个网格项在右上角的网格单元格中重叠。哪一个在上面取决于我们稍后将讨论的层叠行为,但现在,就当它们确实是层叠的事实而言。

有时候你可能希望网格项重叠。例如,照片的标题可能部分重叠在照片上。或者您可能希望将几个项目分配给同一个网格区域,以便它们合并,或者通过脚本或用户交互逐个显示。

重叠不仅限于涉及原始网格数的情况。在以下情况中,侧边栏和页脚将会重叠,如图 12-43 所示。(假设页脚在标记中位于侧边栏之后,则在没有其他样式的情况下,页脚将覆盖在侧边栏之上。)

#grid {display: grid;
    grid-template-areas:
        "header header"
        "sidebar content"
        "footer footer";}
#header {grid-area: header;}
#sidebar {grid-area: sidebar / sidebar / footer-end / sidebar;}
#footer {grid-area: footer;}

image

图 12-43. 侧边栏和页脚重叠

我们提到这一点部分是为了警告您可能出现重叠的可能性,同时也是为了过渡到下一个主题。这是一个使网格布局与定位分开的特性,因为它有时可以帮助避免重叠:网格流的概念。

指定网格流

大部分情况下,我们都是在网格上显式放置网格项。如果未显式放置项,则将自动放置到网格中。根据生效的网格流方向,将项放置在第一个适合其的区域中。最简单的情况就是依次填充网格轨道,一个接一个地放置网格项,但情况可能比这复杂得多,尤其是在显式和自动放置的网格项混合存在时。后者必须围绕前者工作。

CSS 主要有两种网格流模型,即行优先列优先,尽管你可以通过指定密集流来增强任何一种流。所有这些都是通过名为grid-auto-flow的属性完成的。

要了解这些值如何工作,请考虑以下标记:

<ol id="grid">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ol>

对于这个标记,让我们应用以下样式:

#grid {display: grid; width: 45em; height: 8em;
    grid-auto-flow: row;}
#grid li {grid-row: auto; grid-column: auto;}

假设网格每隔 15 em 设置一列线,每隔 4 em 设置一行线,我们得到如图 12-44 所示的结果。

image

图 12-44. 行导向的网格流

这可能看起来很正常,就像你将所有框都浮动或者都设置为内联块一样。正是这种熟悉感,使得row成为默认值。现在,让我们尝试将grid-auto-flow的值切换为column,如图 12-45 所示:

#grid {display: grid; width: 45em; height: 8em;
    grid-auto-flow: column;}
#grid li {grid-row: auto; grid-column: auto;}

因此,使用grid-auto-flow: row时,每行都会先填满,然后再开始下一行。而使用grid-auto-flow: column时,每列会先填满。

image

图 12-45. 列导向的网格流

这里需要强调的是,列表项并未显式设置大小。默认情况下,它们被调整大小以连接到定义的网格线。可以通过为元素分配显式大小来覆盖此行为。例如,如果我们将列表项设置为宽 7 em 高 1.5 em,则会得到如图 12-46 所示的结果:

#grid {display: grid; width: 45em; height: 8em;
    grid-auto-flow: column;}
#grid li {grid-row: auto; grid-column: auto;
    width: 7em; height: 1.5em;}

image

图 12-46. 显式大小的网格项

如果你将其与之前的图进行比较,会发现对应的网格项起始位置相同;只是终止位置不同。这说明,真正放置在网格流中的是网格区域,然后将网格项连接到这些区域。

如果您自动流动的元素比其分配的列宽更宽或比其分配的行高更高,这一点很重要,这在将图像或其他固有尺寸的元素转换为网格项时非常容易发生。假设我们想要将一堆不同大小的图像放入一个网格中,该网格设置为每 50 水平像素一个列线,每 50 垂直像素一个行线。这个网格在图 12-47 中有所说明,并显示了通过行或列流动一系列图像的结果:

#grid {display: grid;
    grid-template-rows: repeat(3, 50px);
    grid-template-columns: repeat(4, 50px);
    grid-auto-rows: 50px;
    grid-auto-columns: 50px;
}
img {grid-row: auto; grid-column: auto;}

image

图 12-47. 网格中的流动图像

注意到一些图像重叠在一起了吗?这是因为每个图像都附加到流动中的下一个网格线,而不考虑其他网格项的存在。当它们需要时,我们没有设置图像跨越多个网格轨道,因此发生了重叠。

这可以通过类名或其他标识符来管理。我们可以将图像分类为tallwide(或两者兼有),并指定它们获得更多的网格轨道。以下是添加到前面示例中的一些 CSS,其结果显示在图 12-48 中:

img.wide {grid-column: auto / span 2;}
img.tall {grid-row: auto / span 2;}

css5 1248

图 12-48. 给图像更多的跟踪空间

这确实导致图像继续在页面上溢出,但不会发生重叠。

然而,注意到这个网格中的空隙?这是因为跨越网格线放置一些网格项不会为流动中的其他项留下足够的空间。为了更清楚地说明这一点和两种流动模式,让我们通过一个带有编号框的例子来试一试(参见图 12-49)。

image

图 12-49. 表示流动模式

沿着第一个网格的行进行计数。在这个特定的流动中,网格项的布局几乎就像它们是左浮动的一样。几乎,但并非完全如此:请注意,网格项 13 实际上位于网格项 11 的左侧。这在浮动时永远不会发生,但在网格流中却可以。行流(如果我们可以这样称呼它)的工作方式是从左到右沿每一行进行,如果有空间放置网格项,则将其放置在那里。如果一个网格单元格已经被另一个网格项占据,则跳过它。因此,旁边的项目 10 的单元格没有填充,因为没有足够的空间放置项目 11。项目 13 之所以位于项目 11 的左侧,是因为当到达行时那里有足够的空间放置它。

正如在图 12-49 的第二个例子中所示,列流的基本机制同样适用,只是这种情况下你从上到下操作。因此,项目 9 下面的单元格是空的,因为项目 10 无法放在那里。相反,项目 10 进入了下一列并覆盖了四个网格单元格(每个方向两个)。之后的项目,由于它们只有一个网格单元大小,按列顺序填充其后的单元格。

注意

网格流在左到右,从上到下的语言中运行。在具有该书写模式的 RTL 语言(如阿拉伯语和希伯来语)中,面向行的流将从右到左运行,而不是从左到右。

如果您现在希望以尽可能密集的方式打包网格项,而不考虑这如何影响排序,好消息:您可以!只需将关键字dense添加到您的grid-auto-flow值中,就会发生这种情况。我们可以在图 12-50 中看到结果,该图显示了grid-auto-flow: row densegrid-auto-flow: dense column并排显示的结果。

image

图 12-50。展示密集流模式

在第一个网格中,项目 12 出现在项目 11 上方,因为有一个适合它的单元格。出于同样的原因,在第二个网格中,项目 11 出现在项目 10 的左侧。

实际上,使用dense网格流时,对于每个网格项,浏览器会沿着给定流方向(rowcolumn)从流的起点(从左到右语言的左上角)开始扫描整个网格,直到找到一个适合该网格项的位置。这可以使诸如照片库之类的东西更紧凑,并且在没有网格项需要出现的特定顺序时效果非常好。

现在我们已经探讨了网格流,我们有一个坦白话要说:为了使最后几个网格项看起来正确,我们包含了一些 CSS 代码,我们没有向您展示。如果没有它,突出显示网格边缘的项目看起来会与其他项目大不相同——在面向行的流中会更短,在面向列的流中会更窄。在下一节中,您将看到原因及我们使用的 CSS。

定义自动网格轨道

到目前为止,我们几乎完全看到将网格项放置到明确定义的网格中。但在前一节中,我们的网格项超出了明确定义的网格的边缘。当网格项超出边缘时会发生什么?根据需要添加行或列以满足所述项目的布局指令(请参见“使用隐式网格”)。因此,如果在面向行的网格的末尾之后添加具有行跨度3的项目,则会在明确定义的网格后添加三行。

默认情况下,这些自动添加的网格轨道是所需的绝对最小尺寸。如果您想对它们的大小施加更多控制,grid-auto-rowsgrid-auto-columns就是为您设计的。

对于任何自动创建的行或列轨道,您可以提供单个轨道大小或 minmaxed 轨道大小对。让我们看看上一节中网格流示例的简化版本:我们将设置一个 2×2 网格,并尝试将五个项目放入其中。实际上,我们会做两次:一次使用grid-auto-rows,一次不使用,如图 12-51 所示:

.grid {display: grid;
    grid-template-rows: 80px 80px;
    grid-template-columns: 80px 80px;}
#g1 {grid-auto-rows: 80px;}

如第二个网格所示,如果没有为自动创建的行指定大小,则溢出的网格项将放置在与网格项内容完全相同高度的行中,而不多一像素。每个项仍然与放置它们的列一样宽(80px),因为列已经定义了大小。行,由于缺乏显式高度,默认为auto,结果如图所示。

css5 1251

Figure 12-51. 具有自动行大小和无自动行大小的网格

如果我们将事物转换为基于列的流,则相同的基本原则适用(参见图 12-52):

.grid {display: grid; grid-auto-flow: column;
    grid-template-rows: 80px 80px;
    grid-template-columns: 80px 80px;}
#g1 {grid-auto-columns: 80px;}

image

Figure 12-52. 具有自动列大小和无自动列大小的网格

在这种情况下,因为流是基于列的,所以最后的网格项被放置到明确网格末端之外的新列中。在第二个网格中,没有grid-auto-columns,第五和第六个项的每个都与它们的行一样高(80px),但宽度为auto,因此它们的宽度正好适合它们所需的宽度,不再宽。

现在你知道我们在前一节中使用grid-auto-flow图表中使用了什么:我们悄悄地使自动行和自动列的大小与明确指定的列相同,以避免最后几个网格项看起来奇怪。让我们重新带回其中一个图表,只是这次将删除grid-auto-rowsgrid-auto-columns样式。如图 12-53 所示,每个网格中的最后几个项比其余的短或窄,因为缺乏自动跟踪大小。

image

Figure 12-53. 移除了自动跟踪大小的前一个图表

现在你知道了……剩下的故事。

使用网格简写

终于,我们来到了grid的简写属性。它可能会让你感到意外,因为它不像其他简写属性。

语法有点令人头痛,没错,但我们将逐步解释它。

让我们直接谈谈房间里的大象:grid允许你定义网格模板使用紧凑语法设置网格的流和自动跟踪大小。你不能同时做这两件事。

此外,你不定义的部分将被重置为默认值,这对于缩写属性是正常的。因此,如果你定义了网格模板,流和自动跟踪将返回到它们的默认值。

现在让我们讨论如何通过使用grid创建网格模板。值可以变得非常复杂,并采用一些有趣的模式,但在某些情况下非常有用。例如,以下规则等同于随后的一组规则:

grid:
    "header header header header" 3em
    ". content sidebar ." 1fr
    "footer footer footer footer" 5em /
    2em 3fr minmax(10em,1fr) 2em;

/* the following together say the same thing as above */
grid-template-areas:
    "header header header header"
    ". content sidebar ."
    "footer footer footer footer";
grid-template-rows: 3em 1fr 5em;
grid-template-columns: 2em 3fr minmax(10em,1fr) 2em;

注意grid-template-rows的值是如何在grid-template-areas字符串中分散并散布的。这就是在grid中处理行大小时处理网格区域字符串时的方式。去掉这些字符串,你会得到以下结果:

grid:
     3em 1fr 5em / 2em 3fr minmax(10em,1fr) 2em;

换句话说,行轨道与列轨道之间由斜线(/)分隔。

记住,使用grid时,未声明的简写会重置为它们的默认值。这意味着以下两条规则是等效的:

#layout {display: grid;
    grid: 3em 1fr 5em / 2em 3fr minmax(10em,1fr) 2em;}

#layout {display: grid;
    grid: 3em 1fr 5em / 2em 3fr minmax(10em,1fr) 2em;
    grid-auto-rows: auto;
    grid-auto-columns: auto;
    grid-auto-flow: row;}

因此,请确保您的grid声明出现在与定义网格相关的任何其他内容之前。如果我们想要密集的列流,则应编写类似于这样的内容:

#layout {display: grid;
    grid: 3em 1fr 5em / 2em 3fr minmax(10em,1fr) 2em;
    grid-auto-flow: dense column;}

现在,让我们重新引入命名网格区域,并同时添加一些额外的行网格线名称。一个位于行轨道上方的命名网格线在字符串之前写入,而位于行轨道下方的网格线在字符串和任何轨道大小之后写入。因此,假设我们想在中间行上方和下方添加main-startmain-stop,并在底部添加page-end

grid:
    "header header header header" 3em
    [main-start] ". content sidebar ." 1fr [main-stop]
    "footer footer footer footer" 5em [page-end] /
    2em 3fr minmax(10em,1fr) 2em;

这将创建图 12-54 所示的网格,包括隐式创建的命名网格线(例如,footer-start),以及我们写入 CSS 中的显式命名网格线。

image

图 12-54. 使用grid简写创建网格

您可以看到,grid值可以非常快速地变得非常复杂。这是一种强大的语法,一旦您稍微练习一下,就会惊讶地发现很容易上手。另一方面,如果操作有误,整个值可能会无效,从而导致没有任何网格出现。

对于grid的另一种用法,它是grid-auto-flowgrid-auto-rowsgrid-auto-columns的合并。以下规则是等效的:

#layout {grid-auto-flow: dense rows;
    grid-auto-rows: 2em;
    grid-auto-columns: minmax(1em,3em);}

#layout {grid: dense rows 2em / minmax(1em,3em);}

这对于获得相同结果来说,确实是少打了很多字!但我们再次必须提醒您:如果您编写这个,所有列和行轨道属性将被设置为它们的默认值。因此,以下规则是等效的:

#layout {grid: dense rows 2em / minmax(1em,3em);}

#layout {grid: dense rows 2em / minmax(1em,3em);
	grid-template-rows: auto;
	grid-template-columns: auto;}

因此,再次强调,确保您的简写出现在可能覆盖的任何属性之前是很重要的。

使用子网格

我们许诺很久以前要讨论subgrid,现在终于到了时候。基本总结是,子网格 是使用祖先网格的网格轨道来对齐其网格项的网格,而不是使用独特于自身的模式。一个简单的例子是在<body>元素上设置一些列,然后让所有布局组件都使用该网格,无论它们在标记中的深度如何。

让我们看看它是如何工作的。我们将从类似这样的简单标记结构开始:

<body>
  <header class="site">
     <h1>ConHugeCo</h1>
     <nav>…</nav>
  </header>
  <main>
     …
  </main>
  <footer class="site">
     <img src="…" class="logo">
     <nav>…</nav>
     <div>…</div>
  </footer>
</body>

一个真实的主页会有更多元素,但为了清晰起见,我们保持这样的简洁。

首先,我们添加以下 CSS:

body {display: grid; grid-template-columns: repeat(15,1fr);}

此时,主体具有 15 列,每列大小均等,这要归功于1fr的值。这些列由 14 个沟槽分隔,每个沟槽宽度为视口宽度的 1%。(这几乎肯定是桌面样式,而非移动设备的样式。)

目前,<body> 元素的三个子元素正试图塞进这 15 列中的前 3 列。我们不希望这样:我们希望它们跨越整个布局的宽度。嗯,我们希望头部和尾部这样做。<main> 元素实际上应该从视口边缘向外部分开一列。

因此我们添加以下 CSS:

:is(header, footer).site {grid-column: 1 / -1;}
main {grid-column: 2 / -2;}

到目前为止,我们所做的在图 12-55 中有所体现(增加了虚线来表示为 <body> 元素设置的网格列轨道),还有一些在最初的标记代码中不存在的额外内容。(您很快会看到更详细的内容。)

css5 1255

图 12-55. 页面布局的初始设置

这看起来可能是一个完全无意义的定义和忽略一堆网格列的练习,但请稍等。情况即将变得很有趣。

让我们更仔细地查看网站的头部。这里是它完整的标记结构,去掉链接 URL:

<header class="site">
  <h1>ConHugeCo Industries</h1>
  <nav>
     <a href="…">Home</a>
     <a href="…">Mission</a>
     <a href="…">Products</a>
     <a href="…">Services</a>
     <a href="…">Support</a>
     <a href="…">Contact</a>
  </nav>
</header>

再次强调,一个真实的网站可能会有更多内容,但这已足以阐明观点。现在我们要做的是将 <header> 元素转变为一个网格容器,它使用 <body> 元素的网格轨道来自己布局:

header.site {display: grid; grid-template-columns: subgrid;}
header.site h1 {grid-column: 2 / span 5;}
header.site nav {grid-column: span 7 / -2;
     align-self: center; text-align: end;}

在第一条规则中,我们使用 display: grid 将元素设为网格容器,然后指定它的列模板为 subgrid。此时,浏览器会沿着标记树向上查找最近的网格容器,并使用那个祖先元素(在本例中是 <body>)的 grid-template-columns。但这并不仅仅是值的复制。<header> 元素实际上是在使用 body 的网格轨道来进行列向布局。

因此,当第二条规则指定 <h1> 应从第 2 列线开始并跨越五个列轨道时,它实际上是从 body 的第二列线开始,并跨越 body 的五个列轨道。类似地,<nav> 元素被设置为跨越从 body 倒数第二列线开始的七个轨道。图 12-56 展示了结果,同时展示了 <nav> 元素的自对齐和文本对齐,以及一些阴影背景来清楚地指示头部的部件是如何进行网格化的。

css5 1256

图 12-56. 将 header 的部件放置在 body 的列上

注意,在头部内部的各个部件与 <main> 元素的边缘完美对齐。这是因为它们都放置在完全相同的网格线上。不是偶然重合的分离网格线,而是实际的网格线。这意味着,例如,如果将 <body> 元素的列模板更改为添加几列,或者调整某些列的宽度变窄或变宽,我们只需编辑 <body>grid-template-columns 值,那些使用这些列线的所有内容都将随着线移动。

我们可以用 footer 做类似的事情。例如,看看这个 CSS:

footer.site {display: grid; grid-template-columns: subgrid;}
footer.site img {grid-column: 5;}
footer.site nav {grid-column: 9 / -4; }
footer.site div {grid-column: span 2 / -1;}

现在页脚中的徽标被放置在第五列线旁边,<nav> 从布局中心的列线开始,并跨越几个轨道,包含法律内容的 <div> 则结束于最后一列线,并跨越两个轨道。图 12-57 显示了结果。

css5 1257

看起来,也许我们希望法律条款放在导航链接下面。在这种情况下通常的解决方案是将导航链接和法律条文包装到 <div> 等容器中,然后将该容器放置在网格列上。但由于 subgrid 的工作方式,这一点并不必要!

定义显式轨道

放置页脚部分在其他内容下方的更类似于网格的解决方案是将它们放在自己的行上。所以让我们这样做:

footer.site {display: grid; grid-template-columns: subgrid;
     grid-template-rows: repeat(2,auto);}
footer.site img {grid-column: 5; grid-row: 1 / -1;}
footer.site nav {grid-column: 9 / -2; }
footer.site div {grid-column: span 7 / -2; grid-row: 2;}

与上次查看相比,此代码只有三个新内容。首先,<footer> 本身被赋予了 grid-template-rows 值。其次,徽标图像设置为跨越第一条规则定义的两行。第三,<div>grid-column 值被更改,使其跨越与 <nav> 相同的列轨道。只是表达方式不同。<div> 也被设置为显式网格行。

因此,虽然 <footer> 继续对 body 元素的列模板进行子网格化,但它也定义了自己的私有行模板。在这种情况下只有两行,但这已经足够了。图 12-58 显示了结果,并添加了虚线以显示 <footer> 的两行之间的边界。

css5 1258

处理偏移

现在让我们看看文档中的 <main> 元素,它包含了这个基本的标记:

<main>
     <div class="gallery">
          <div>
               <img src="…" alt="">
               <h2>Title</h2>
               <p>Some descriptive text</p>
          </div>
     </div>
</main>

正如你之前看到的,<main> 元素如下放置在 <body> 的网格上:

main {grid-column: 2 / -2;}

这导致它从 <body> 的第二列网格线延伸到倒数第二列网格线。这将使其两侧向内推进一列。

<main> 元素内的内容不参与 <body> 的网格,因为 <main> 不是子网格。好吧,还不是。让我们通过将规则更改为以下内容来解决这个问题,并且结果如 图 12-59 所示。

css5 1259

图 12-59. 将 <main> 元素的子项放置在 <body> 的网格上

再次,这个元素是 body 的子网格,但这次它没有从网格的一边拉伸到另一边。画廊 <div> 只占据了一列,因为它是一个没有分配任何网格列值的网格项。

所以问题是:如果我们想把它移动到离 <main> 元素边缘一列的地方,怎么办?那就是 <body> 的第三列线,但在 <main> 元素的容器内部是第二个。应该是 grid-column: 3 还是 grid-column: 2

答案是 2。在计算子网格内的网格线时,只计算其中的网格线。因此,以下将显示 图 12-60 中的结果:

.gallery {grid-column: 2 / -2;}

现在,该图库填充了除 <main> 容器的起始列和结束列之外的所有列,从 <main> 的第二个网格线开始,到倒数第二个网格线结束。如果我们将值更改为 3 / -3,则图库将从 <main> 的第三列线延伸到倒数第三列线,因此在两侧留下两个空列。但我们不要这样做。

css5 1260

图 12-60. 在每侧向内移动图库,并跨越多列

相反,现在假设我们向图库添加五张额外的卡片,总共六张,并添加一些填充文本,而不是每张都只有标题“标题”等。如果我们这样做并且不更改任何 CSS,我们将只是有六个 <div> 叠在彼此上,因为虽然图库跨越了 <main> 的子网格,但它不是子网格(甚至不是非子网格),因此其内部是正常流环境。

我们可以通过——是的——更多的子网格化来解决这个问题!

.gallery {grid-column: 2 / -2; display: grid; grid-template-columns: subgrid;}

现在,该图库是其最近的祖先元素的子网格,该元素定义了非子网格化列模板,即 <body> 元素,因此图库内的卡片将使用 <body> 的列模板。我们希望它们填满包含 12 个轨道的图库,因此我们将使它们每个都跨越 2 个轨道,结果显示在 图 12-61 中:

.gallery > div {grid-column: span 2; padding: 0.5em;
     border: 1px solid; background: #FFF8;}

css5 1261

图 12-61. 向子网格化的图库添加多张卡片

不错,但还有改进的空间。最后一张卡片的标题较长,跨两行显示。这意味着所有描述性文本段落的对齐不一致。我们如何解决这个问题呢?与页脚一样:通过为图库定义行模板,并使卡片子网格化到该行模板!

我们首先通过定义一些命名行和轨道大小来定义行模板:

.gallery {display: grid;
     grid-template-columns: subgrid;
	grid-template-rows: [pic] max-content [title] max-content [desc] auto;
     grid-column: 2 / -2;}

现在,每张卡片都需要跨越行模板,以便行线对其可用:

.gallery > div {grid-column: span 2;
     grid-row: 1 / -1;}

现在,卡片从图库的第一行线延伸到最后一行,我们准备将卡片变成具有单列和图库行模板子网格的网格容器:

.gallery > div {grid-column: span 2;
     grid-row: 1 / -1;
     display: grid;
     grid-template-rows: subgrid;
     grid-template-columns: 1fr;}

我们其实并不需要添加 grid-template-columns 声明,因为它默认为单列,但有时明确说明你想要发生的事情是很好的,这样在你之后负责 CSS 的人(包括六个月后的你)就不必猜测你打算做什么。

目前,每个卡片内部的元素将自动落入行轨道:图像落入pic轨道,标题落入title轨道,段落落入desc轨道。但由于我们正在尝试自文档化,让我们明确地将每个元素分配给其命名轨道,并顺便垂直对齐标题。

.gallery > div img {grid-row: pic;}
.gallery > div h2 {grid-row: title; align-self: center;}
.gallery > div p {grid-row: desc;}

图 12-62 显示了最终结果,标题在垂直方向上相对于彼此居中,描述段落都沿其顶边对齐,并且所有卡片都具有相同的高度。

这里的一个重要优势是,通过明确地将卡片的部件分配给命名网格行线,现在重新排列卡片只是编辑设置在图库上的grid-row-template值的问题。

css5 1262

图 12-62. 将卡片项放置在子网格行上

我们还可以将卡片的列模板设置为子网格,这意味着它们将使用<body>元素的列模板,因为body是最近的祖先元素,其列模板不是子网格。在这种情况下,卡片将使用图库的行模板和body的列模板。它们将影响那些祖先网格轨道的尺寸,并因此影响使用相同模板的其他所有内容的布局。

如果您的卡片超过了可以容纳的单行,则会遇到问题:子网格不会创建隐式网格轨道。相反,您需要使用grid-auto-rows等自动跟踪属性,这将添加所需的行数。

因此,我们需要删除线名,并重新构建我们已经建立的 CSS,如下所示:

.gallery {display: grid;
     grid-template-columns: subgrid;
	grid-auto-rows: max-content max-content auto;
	/* was: [pic] max-content [title] max-content [desc] auto */
     grid-column: 2 / -2;}
.gallery > div {grid-column: span 2;
     grid-row: 1 / -1;
     display: grid;
     grid-template-rows: subgrid;
     grid-template-columns: 1fr;}

现在的问题是,图片、标题和描述文本各自分配到一个命名网格线,但grid-auto-rows不允许线名。看起来我们需要更改网格行分配,但事实并非如此,接下来您将看到。

命名子网格行

除了在祖先模板中使用任何网格行的名称外,还可以为子网格分配名称,如果您使用了前一节中创建的自动跟踪,则这真的很有帮助。

在这种情况下,因为我们曾在父网格中使用pictitledesc作为行线名称,但必须删除它们以设置自动行,现在我们将这些相同的标签放在grid-template-rowssubgrid关键字之后:

grid-template-rows: subgrid [pic] [title] [desc];

这是这些卡片的其余 CSS 的上下文示例,其布局如图 12-63 所示:

.gallery {display: grid;
     grid-template-columns: subgrid;
	grid-auto-rows: max-content max-content auto;
     grid-column: 2 / -2;}
.gallery > div {grid-column: span 2;
     grid-row: 1 / -1;
     display: grid;
     grid-template-rows: subgrid [pic] [title] [desc];
     grid-template-columns: 1fr;}
.gallery > div img {grid-row: pic;}
.gallery > div h2 {grid-row: title; align-self: center;}
.gallery > div p {grid-row: desc;}

css5 1263

图 12-63. 将卡片放置到具有命名行的自动行上

还可以仅对几行分配名称,而不对其余行进行命名。为了看到它的作用,让我们在图库下方添加几段文本,类似于以下内容(省略号包围的文本替代了实际内容):

<main>
	<div class="gallery">
		*`…cards here…`*
	</div>
	<p class="leadin">…text…</p>
	<p class="explore">…text…</p>
</main>

要跨越各种列轨道来扩展段落,我们可以计算并使用数字,但让我们命名一些行并使用它们代替。在这种情况下,由于这些段落是<main>元素的子元素,我们需要修改其子网格列模板。这是我们将要做的:

main {grid-column: 2 / -2;
     display: grid;
     grid-template-columns:
          subgrid [] [leadin-start] repeat(5, [])
          [leadin-end explore-start] repeat(5, [])
          [explore-end];
     }

好的,哇。刚刚发生了什么?

下面是它的详细过程:在subgrid关键字之后,我们有一堆名称分配。首先是[],这意味着“不要向此网格线添加名称”。然后是[leadin-start],它将名称leadin-start分配给子网格的第二个网格列线。之后是一次重复,表示接下来的五个网格列线没有子网格名称分配。

接下来是在网格中间运行的线,它被赋予了leadin-endexplore-start的名称。这意味着引导段落应该在这条线停止跨越,并且探索段落应该从同一条线开始跨越。再经过另外五条未命名的线后,我们将explore-end分配给一条线,就这样。任何未被解决的线都将被保留。

现在我们所要做的就是设置段落的起始和结束列线,如此一来,我们得到的结果将显示在图 12-64 中,其中第二行卡片上的两张卡片已经被移除以便清晰地展示:

p.leadin  {grid-column: leadin-start /  leadin-end;}
p.explore {grid-column: explore-start / explore-end;}

css5 1264

图 12-64. 使用子网格命名网格线放置元素

然后它们就在那里了,使用它们自定义命名的起始和结束网格线跨越多个网格轨道。正如承诺的那样,第一个在第二个开始的地方结束,正好在布局中间的那条网格线上。

将卡片紧紧靠在一起看起来不太好,尽管如此。我们可以在段落上使用填充将实际文本分开,但某些间隙会更好,不是吗?

给子网格设置它们自己的间隙

可以在子网格上设置与其祖先网格上的任何间隙分开的间隙。因此,例如,我们可以像这样扩展我们之前的示例:

main {grid-column: 2 / -2;
     display: grid;
     grid-template-columns:
          subgrid [] [leadin-start] repeat(5, [])
          [leadin-end explore-start] repeat(5, [])
          [explore-end];
     gap: 0 2em;
     }

通过这个变化,<main>元素设置了没有行间隙但有 2em 列间隙。这就是在图 12-65 中显示的结果。

css5 1265

图 12-65. 向子网格添加间隙的效果

请注意,不仅两个段落被分开,画廊中的卡片也是如此。这是因为它们都参与同一个子网格,并且该子网格刚刚添加了一些间隙。这意味着卡片的边缘和段落的边缘仍然精确地对齐,这非常好。

还要注意,这些间隙不适用于祖先网格中的内容:页眉和页脚中的框框仍然直接到达中心列线。只有<main>元素的子网格以及该子网格的任何子网格会知道并利用这些间隙。

注意

如果你对间隙不熟悉,那么row-gapcolumn-gapgap的属性在第十一章有详细介绍。

网格项目和盒子模型

现在我们可以创建一个网格,将项目附加到网格上,创建网格跟踪之间的间隙,甚至使用祖先元素的跟踪模板。但是,如果我们用边距样式化网格项目,或者如果它被绝对定位了,会发生什么?这些情况如何与网格线互动?

首先让我们看一下边距。工作的基本原则是元素通过其边距边缘连接到网格。这意味着您可以通过设置正边距将元素的可见部分从其占据的网格区域向内推移,并通过负边距向外拉。例如,这些样式将产生图 12-66 中显示的结果:

#grid {display: grid;
    grid-template-rows: repeat(2, 100px);
    grid-template-columns: repeat(2, 200px);}
.box02 {margin: 25px;}
.box03 {margin: -25px 0;}

css5 1266

图 12-66. 带有边距的网格项目

这样做的原因是项目的widthheight都设置为auto,因此它们可以根据需要拉伸以使所有计算正确。如果width和/或height有非auto的值,它们将覆盖边距以使所有数学计算正确。这很像内联边距在元素大小超出约束时发生的情况:最终,其中一个边距会被覆盖。

考虑一个具有以下样式的元素放置在一个 200 像素宽、100 像素高的网格区域内:

.exel {width: 150px; height: 100px;
    padding: 0; border: 0;
    margin: 10px;}

首先沿着元素走,它的两侧有 10 像素的边距,其width150px,总共是 170 像素。总得有个妥协,这种情况下是右边距(在从左到右的语言中),它被更改为40px以使一切正常——左边距为 10 像素,内容框为 150 像素,右边距为 40 像素,总宽度为网格区域的 200 像素。

在垂直轴上,将底边距重置为-10px。这样做是为了抵消顶部边距和内容高度总计 110 像素,而网格区域只有 100 像素高。

注意

当计算网格跟踪大小时,将忽略网格项目上的边距。因此,无论您将网格项目的边距设置多大或多小,都不会更改min-content列的大小,例如,增加网格项目的边距也不会导致fr大小的网格跟踪大小发生变化。

与块布局一样,您可以选择使用auto边距来决定哪个边距将其值更改以适应。假设我们希望网格项目与其网格区域右对齐。通过将项目的左边距设置为auto,可以实现这一点:

.exel {width: 150px; height: 100px;
    padding: 0; border: 0;
    margin: 10px; margin-left: auto;}

现在元素将为右边距和内容框累加 160 像素,然后将这个值与网格区域宽度之差给左边距,因为它已经明确设置为auto。这导致图 12-67 的结果,exel项目的每侧有 10 像素的边距,除了我们刚刚计算的左边距 40 像素。

image

图 12-67. 使用自动边距对齐项目

这种对齐过程可能在块级布局中看起来很熟悉,其中可以使用auto内联边距将元素居中放置在其包含块中,只要给定了显式的width。网格布局不同之处在于你可以在垂直轴上做同样的事情;也就是说,给定具有绝对高度的元素,你可以通过将顶部和底部边距设置为auto来垂直居中它。图 12-68 展示了对图片应用auto边距的各种效果,这些图片本身具有显式的高度和宽度:

.i01 {margin: 10px;}
.i02 {margin: 10px; margin-left: auto;}
.i03 {margin: auto 10px auto auto;}
.i04 {margin: auto;}
.i05 {margin: auto auto 0 0;}
.i06 {margin: 0 auto;}

image

图 12-68. 各种自动边距对齐方式
提示

CSS 还有其他方法来对齐网格项,特别是使用诸如justify-self之类的属性,这些属性不依赖于具有显式元素大小或自动边距。这些内容将在下一节介绍。

这种自动边距行为与元素绝对定位时边距和元素大小操作的方式非常相似,这就引出了下一个问题:如果网格项是绝对定位怎么办?例如:

.exel {grid-row: 2 / 4; grid-column: 2 / 5;
    position: absolute;
    top: 1em; bottom: 15%;
    left: 35px; right: 1rem;}

答案实际上非常优雅:如果你已定义了网格线的起始和结束并且网格容器建立了定位内容(例如使用position: relative),那么该网格区域将被用作包含块和网格的定位上下文,因此网格项被定位该上下文中。这意味着偏移属性(如top等)是相对于声明的网格区域计算的。因此,前面的 CSS 将会有结果显示在图 12-69,浅色阴影区域表示用作定位上下文的网格区域,而粗边框框表示绝对定位的网格项。

css5 1269

图 12-69. 绝对定位网格项

在这种格式化上下文中,关于偏移、边距、元素大小等所有关于绝对定位元素的知识都适用。只是在这种情况下,格式化上下文由网格区域定义。绝对定位引入了一个变化:它改变了网格线属性中auto值的行为。例如,如果你为一个绝对定位的网格项设置了grid-column-end: auto,那么结束网格线实际上会创建一个新的特殊网格线,该网格线对应于网格容器本身的填充边缘。即使显式网格小于网格容器,也会发生这种情况。为了看到这一过程,我们将修改前面的例子如下,结果显示在图 12-70 中:

.exel {grid-row: 2 / auto; grid-column: 2 / auto;
    position: absolute;
    top: 1em; bottom: 15%;
    left: 35px; right: 1rem;}

css5 1270

图 12-70. 自动值和绝对定位

请注意,定位上下文现在从网格容器的顶部开始(围绕图表外部的细黑线),并且延伸到网格容器的右边缘,尽管网格本身未延伸到该边缘。

这种行为的一个影响是,如果你绝对定位一个作为网格项的元素,但不给它任何网格线的起始或结束值,那么它将使用网格容器的内部填充边缘作为其定位上下文。它可以在不必设置网格容器为 position: relative 或其他通常用于建立定位上下文的技巧的情况下完成这一点。

此外,请注意,绝对定位的网格项 不会 参与计算网格单元和跟踪大小。就网格布局而言,定位的网格项不存在。一旦设置了网格,网格项就相对于定义其定位上下文的网格线进行定位。

设置网格中的对齐

如果你对 flexbox 有所了解(参见第 11 章),你可能已经了解各种对齐属性及其值。这些相同的属性也适用于网格布局,并且具有非常相似的效果。

首先,快速回顾。表格 12-1 总结了可用的对齐属性及其影响范围。请注意,这些属性的数量可能比 flexbox 中的预期更多。

表格 12-1. 对齐和对齐值

属性 对齐 适用于
align-content 整个网格在块方向上 网格容器
align-items 所有网格项在块方向上 网格容器
align-self 网格项在块方向上 网格项
justify-content 整个网格在内联方向上 网格容器
justify-items 所有网格项在内联方向上 网格容器
justify-self 网格项在内联方向上 网格项
place-content 整个网格在块和内联方向上 网格容器
place-items 所有网格项在块和内联方向上 网格容器
place-self 网格项在块和内联方向上 网格项

正如表格 12-1 所示,各种 justify-* 属性改变了内联轴上的对齐方式——用英语描述,这是水平方向。区别在于属性是应用于单个网格项、整个网格中的所有网格项,还是整个网格。类似地,align-* 属性影响块轴上的对齐——用英语描述,这是垂直方向。另一方面,place-* 属性是两个方向上都适用的简写。

对齐和对齐单个项

最容易从 *-self 属性开始,因为我们可以在一个网格中展示各种 justify-self 属性值,而第二个网格展示这些值在 align-self 中使用时的效果(参见图 12-71)。

image

图 12-71. 行内和块方向的自对齐

每个网格项目在图 12-71 中显示其网格区域(虚线)和标签,标识应用于它的属性值。每个都值得一点评论。

首先要意识到,对于所有这些值,任何没有显式 widthheight 的元素都将“收缩包裹”其内容,而不是使用默认的网格项行为填充整个网格区域。

startend 值使网格项与其网格区域的开始或结束边对齐,这是有道理的。类似地,center 将网格项沿对齐轴在其区域内居中,无需声明边距或任何其他属性,包括 heightwidth

leftright 值使项目在行内轴水平时对齐到网格区域的左侧或右侧,如图 12-71 所示。如果行内轴是垂直的,例如 writing-mode: vertical-rl,项目沿行内轴对齐,就像行内轴仍然是水平的一样;因此,在从上到下的行内轴中,当 directionltr 时,left 将对齐到网格区域的顶部,并且在 rtl 时对齐到底部。当应用于 align-self 时,leftright 被视为 stretch

self-startself-end 值更有趣。self-start 选项将网格项与对应于其开始边的网格区域边缘对齐。因此,在图 12-71 中,self-startself-end 盒子设置为 direction: rtl。这设置它们使用右到左语言方向,意味着它们的起始边是右边缘,结束边是左边缘。您可以在第一个网格中看到这个右对齐的 self-start 和左对齐的 self-end。然而,在第二个网格中,RTL 方向对于块轴对齐是无关紧要的。因此,self-start 被视为 startself-end 被视为 end

最后一个值 stretch 也很有趣。要理解它,注意每个网格中的其他框如何“收缩包裹”其内容,就好像设置为 max-content。相比之下,stretch 值则指示元素沿给定方向从边到边拉伸 — align-self: stretch 导致网格项在块轴上拉伸,而 justify-self: stretch 导致行内轴拉伸。这正如您可能期望的那样,但请记住,它仅在元素的大小属性设置为 auto 时才有效。因此,鉴于以下样式,第一个示例将在垂直方向上拉伸,但第二个示例则不会:

.exel01 {align-self: stretch; block-size: auto;}
.exel02 {align-self: stretch; block-size: 50%;}

因为第二个示例设置了非 auto(默认值)的 block-size 值,因此该网格项不能通过 stretch 进行调整大小。对于 justify-selfinline-size 也是如此。

可用于对齐网格项的另外两个值足够有趣,值得单独解释。这些允许将网格项的第一个或最后一个基线与其行轨道中最高或最低的基线对齐。例如,假设您希望网格项对齐,以使其最后一行的基线与共享其行轨道的最高网格项的最后一行基线对齐。那将看起来像这样:

.exel {align-self: last-baseline;}

相反地,要将其第一个基线与同一行轨道中最低的第一个基线对齐,您可以这样说:

.exel {align-self: baseline;}

如果网格元素没有基线,或者要求在不能比较基线的方向上基线对齐自身,则 baseline 被视为 start,并且 last-baseline 被视为 end

注意

此部分有意跳过两个值:flex-startflex-end。这些值仅应在 flexbox 布局中使用,并且在任何其他布局上下文中,包括网格布局中,被定义为等同于 startend

关于刚讨论的值及其如何引起项目之间交互的更详细说明,请参见 第十一章。

缩写属性 place-self 结合了刚讨论的两个自定位属性。

为了 place-self 提供一个值,意味着它也会复制到第二个值中。因此,在以下每对声明中,第一个声明等同于第二个:

place-self: end;
place-self: end end;

因为两个个体属性 place-self 缩写都可以接受基线对齐值,提供一个值会导致两个个体属性都设置为相同的值。换句话说,以下声明是等效的:

place-self: last baseline;
place-self: last baseline last baseline;

您也可以提供两个值,一个用于缩写所代表的每个个体属性。因此,以下 CSS 展示了互为等效的规则:

.gallery > .highlight {place-self: center;}
.gallery > .highlight {align-self: center; justify-self: center;}

对齐和调整所有项

现在让我们考虑align-itemsjustify-items。这些属性接受前面部分中看到的所有相同值以及几个其他值,并且具有相同的效果,除非它们应用于给定网格容器中的所有网格项,并且必须应用于网格容器而不是单个网格项。

例如,您可以将网格中所有网格项设置为在其网格区域内居中对齐,如下所示,结果类似于图 12-72 中所示的效果:

#grid {display: grid;
    align-items: center; justify-items: center;}

image

图 12-72. 所有网格项居中

正如您所见,这条规则水平和垂直地使每个网格项居中于其给定的网格区域。此外,由于center的处理方式,它会导致任何没有显式宽度和高度的网格项“包裹”其内容,而不是拉伸以填充其网格区域。如果网格项具有显式的内联或块大小,则会优先考虑这些大小,而不是“包裹”内容,并且该项仍然位于其网格区域的中心。

想了解各种关键字值在justify-itemsalign-items上下文中的影响概述,请参见图 12-73;网格区域用虚线表示,网格项根据其对齐值放置。

image

图 12-73. 网格单元内网格项的对齐方式

未在图 12-73 中说明的legacy值是网格对齐的一个新补充,基本上被视为start。(它存在以重新创建 HTML 的古老<CENTER>元素和align属性的行为,但在网格上下文中不相关。)

小贴士

想了解safeunsafe在项目溢出其容器方面的含义,请参阅第 11 章。

快捷属性place-items结合了刚刚讨论的两个项目放置属性。

place-items的工作方式与本章前面讨论的place-self属性非常相似。如果给出一个值,则将其应用于align-itemsjustify-items。如果给出两个值,则第一个值应用于align-items,第二个值应用于justify-items。因此,以下规则是等效的:

.gallery {place-items: first baseline start;}
.gallery {align-items: first baseline; justify-items: start;}

分布网格项和轨道

除了对齐和调整每个网格项外,可以使用align-contentjustify-content来分布网格项,甚至可以调整或对齐整个网格。这些属性使用了一小组分布值。图 12-74 展示了应用于justify-content的每个值的效果,每个网格都共享以下样式:

.grid {display: grid; padding: 0.5em; margin: 0.5em 1em; inline-size: auto;
	grid-gap: 0.75em 0.5em; border: 1px solid;
	grid-template-rows: 4em;
	grid-template-columns: repeat(5, 6em);}

image

图 12-74. 沿行内轴分布网格项

在这些情况下,网格跟踪集被视为单个单位,并且然后通过justify-content的值来对齐这些项。这种对齐不影响单个网格项的对齐方式;因此,您可以通过justify-content: end将整个网格右对齐,同时让单个网格项在其网格区域内左对齐、居中对齐或起始对齐(等等)。

这在列跟踪中同样有效,正如图 12-75 所示,只要切换到align-content。这一次,所有网格都共享这些样式:

.grid {display: grid; padding: 0.5em;
	grid-gap: 0.75em 0.5em; border: 1px solid;
	grid-template-rows: repeat(4, 3em);
	grid-template-columns: 5em;}

image

图 12-75. 沿块轴分布网格项

这些分布的工作方式是,包括任何分隔线在内的网格跟踪都像往常一样大小。然后,如果除了网格跟踪和分隔线之外还有空间,也就是说,如果网格跟踪没有从网格容器的一边延伸到另一边,那么剩余的空间将根据justify-content(在内联轴上)或align-content(在块轴上)的值进行分配。

这种空间分布通过调整网格分隔线来实现。如果未声明任何分隔线,则会创建它们。如果已经存在分隔线,则根据需要调整其大小以按指定方式分配网格跟踪。

请注意,因为仅在轨道未填满网格容器时分配空间,因此分隔线只能增加大小。如果轨道大于容器,这很容易发生,那么没有剩余空间可分配(负空间原来是不可分割的)。

在之前的图中未显示的另一个分布值是stretch。此值将任何剩余空间均匀应用于网格跟踪,而不是分隔线。因此,如果我们有 400 像素的剩余空间和 8 个网格跟踪,每个网格跟踪将增加 50 像素。网格跟踪不会按比例增加,而是均匀增加。截至 2022 年底,浏览器尚不支持此值以进行网格分布。

分层和排序

如前面的一节中讨论的那样,完全可以让网格项彼此重叠,无论是因为使用负边距将网格项拉到其网格区域边缘之外,还是因为两个不同网格项的网格区域共享网格单元格。默认情况下,文档源顺序中的网格项将在视觉上重叠:文档源中后面的网格项将显示在文档源中前面的网格项上面(或“在前面”)。因此,以下内容将导致图 12-76 中的结果(假设每个类名中的数字代表网格项的源顺序):

#grid {display: grid; width: 80%; height: 20em;
    grid-rows: repeat(10, 1fr); grid-columns: repeat(10, 1fr);}
.box01 {grid-row: 1 / span 4; grid-column: 1 / span 4;}
.box02 {grid-row: 4 / span 4; grid-column: 4 / span 4;}
.box03 {grid-row: 7 / span 4; grid-column: 7 / span 4;}
.box04 {grid-row: 4 / span 7; grid-column: 3 / span 2;}
.box05 {grid-row: 2 / span 3; grid-column: 4 / span 5;}

image

图 12-76. 文档源顺序中的网格项重叠

如果您想要确立自己的堆叠顺序,z-index会帮助您。就像在定位中一样,z-index将元素相对于 z 轴上的其他元素定位,该轴垂直于显示表面。正值靠近您,负值远离您。因此,要将第二个框带到“顶部”,您只需要给它一个比任何其他值更高的z-index值(结果显示在图 12-77 中):

.box02 {z-index: 10;}

image

图 12-77. 提升网格项

另一种影响网格项排序的方法是使用order属性。其效果基本上与在 flexbox 中相同——您可以通过给它们分配order值来更改网格轨道内的网格项顺序。这不仅影响轨道内的放置,还影响绘制顺序,如果它们重叠的话。例如,我们可以将前面的示例从z-index改为order,如下所示,并得到与图 12-77 中显示的相同结果:

.box02 {order: 10;}

在这种情况下,box02之所以出现在其他网格项“上方”,是因为它的顺序使其排在它们之后。因此,它最后绘制。同样地,如果这些网格项在网格轨道中按顺序放置,那么box02order值将把它放在序列的末尾。这在图 12-78 中有所描述。

image

图 12-78. 更改网格项顺序

记住,仅仅因为您可以以这种方式重新排列网格项的顺序,并不一定意味着您应该这样做。正如网格布局规范所述:

与重新排序 flex 项一样,只有在视觉顺序需要与语音和导航顺序不一致时,才应该使用order属性;否则应该重新排序底层文档源。

因此,使用order重新排列网格项布局的唯一原因是,如果您需要将文档源按一种顺序,布局按另一种顺序,这已经可以通过将网格项分配到不匹配源顺序的区域来轻松实现。

这并不是说order是无用的,总是应该避免使用;可能会有时候它是有意义的。但除非特定情况几乎迫使您使用它,否则请认真考虑它是否是最佳解决方案。

提示

要了解order属性的正式定义,请参见第十一章。

概要

网格布局是复杂而强大的,所以如果一开始感到不知所措,请不要灰心。要习惯网格操作的方式需要一些时间,特别是因为它的许多特性与我们之前处理的内容完全不同。这些功能的大部分力量直接来源于它们的新颖性——但像任何强大的工具一样,网格布局学习起来可能会很困难和令人沮丧。

我们希望我们能帮助你避开一些陷阱,但是,请记住尤达大师的智慧:“你必须忘记你所学过的。”当涉及到网格布局时,从来没有比现在更需要抛开你对布局的既有认知,重新学习的时刻。随着时间的推移,你的耐心和坚持将会得到回报。

第十三章:CSS 中的表格布局

您可能已经看过本章标题,并想知道,“表格布局?这不是上个千年的东西吗?”确实如此,但本章不是关于如何使用表格来进行布局的。相反,它讲述了 CSS 如何布局表格本身,这比看起来复杂得多。

表格与文档布局的其余部分相比较显得不寻常。直到 flexbox 和 grid 出现之前,表格是唯一能够将元素大小与其他元素关联起来的工具。例如,行中的所有单元格具有相同的高度,无论每个单元格中的内容多少。同样,共享列的单元格也具有相同的宽度。相邻的单元格可以共享边框,即使这两个单元格具有非常不同的边框样式。正如您将看到的那样,这些能力是以牺牲许多行为和规则为代价的——其中许多根源深深植根于网络的过去,这些规则只适用于表格,而不是其他元素。

表格格式化

在我们开始担心如何绘制单元格边框和调整表格大小之前,我们需要深入探讨组装表格和表格中元素关联的基本方法。这被称为 表格格式化,它与表格布局完全不同:只有在完成格式化之后,布局才成为可能。

视觉上安排表格

首先要理解的是 CSS 如何定义表格的布局。虽然这些知识可能看起来很基础,但它是理解如何最好地样式化表格的关键。

CSS 区分 表格元素内部表格元素。在 CSS 中,内部表格元素生成具有内容、内边距和边框的矩形框,但没有外边距。因此,无法通过给单元格、行或任何其他内部表格元素(除了标题,详情请见 “使用标题”)应用外边距来定义表格单元格之间的分隔。符合 CSS 的浏览器会忽略任何尝试为单元格、行或任何其他内部表格元素应用外边距的操作。

CSS 有六条基本规则来排列表格。这些规则的基础是 网格单元格,它是表格绘制时网格线之间的一个区域。请考虑图中两个表格,它们的网格单元格由虚线表示。

image

图 13-1. 网格单元格形成表格布局的基础

在一个简单的 2 × 2 表格中,例如图中左侧的表格,网格单元格对应于实际的表格单元格。在一个更复杂的表格中,例如图中右侧的表格,一些表格单元格将跨越多个网格单元格——但请注意,每个表格单元格的边缘都位于网格单元格的边缘上。

这些网格单元格在很大程度上是理论构造,无法进行样式化,甚至不能通过 DOM 访问。它们只是一种描述表格组装样式的方式。

表格布局规则

表格布局的六大规则如下:

  • 每个行框包含一行网格单元格。表中的所有行框按照它们在源文档中出现的顺序从上到下填充表格(除了任何表头或表尾行框,它们分别出现在表格的开头和结尾)。因此,表包含与行元素(例如 <tr> 元素)数量相同的网格行。

  • 行组的框包含与其包含的行框中相同的网格单元格。

  • 列框包含一个或多个列的网格单元格。所有列框按照它们出现的顺序放置在一起。对于 LTR 语言,第一个列框位于左侧,对于 RTL 语言则位于右侧。

  • 列组的框包含与其包含的列框中相同的网格单元格。

  • 尽管单元格可以跨越多行或多列,但 CSS 并未定义这种跨越发生的方式。这完全取决于文档语言来定义跨越。每个跨越的单元格都是一个矩形框,宽度和高度可以是一个或多个网格单元格。这个跨越矩形的顶部行位于其父网格单元格所在的行中。单元格的矩形框必须尽可能地在 LTR 语言中向左边,但不得重叠任何其他单元格框。它还必须位于同一行中源文档中较早出现的所有单元格的右侧(在 LTR 语言中)。在 RTL 语言中,跨越的单元格必须尽可能地在右侧,而不重叠其他单元格,并且必须位于源文档中其后的所有同一行中的所有单元格的左侧

  • 单元格的框不能超出表格或行组的最后一个行框。如果表格结构会导致这种情况,则必须缩短单元格,直到它适合包含它的表格或行组内。

注意

CSS 规范不鼓励,但也不禁止,定位表格单元格和其他内部表格元素。例如,对包含跨行单元格的行进行定位可能会通过将该行从表格中完全移除来显著改变表格的布局,从而在其他行的布局中排除跨越的单元格。尽管如此,目前的浏览器确实可以对表格元素应用定位。

根据定义,网格单元格是矩形的,但它们不必都是相同大小的。同一网格列中的所有网格单元格将具有相同的宽度,同一网格行中的所有网格单元格将具有相同的高度,但一个网格行的高度可能与另一个网格行的高度不同。类似地,网格列的宽度可以不同。

在掌握这些基本规则后,可能会有一个问题:到底如何确定哪些元素是单元格,哪些不是?

设置表格显示值

在 HTML 中,很容易知道哪些元素是表格的一部分,因为像<tr><td>这样的元素处理已经内置在浏览器中。另一方面,在 XML 中,没有办法从本质上知道哪些元素可能是表格的一部分。这就是display的整套值发挥作用的地方。

在本章中,我们将专注于与表格相关的值,其他值超出了表格的范围。与表格相关的值可以总结如下:

table

定义块级表格。因此,它定义了一个生成块框的矩形块。对应的 HTML 元素是<table>

inline-table

定义内联级表格。这意味着元素定义了一个生成内联框的矩形块。最接近的非表格类比是值inline-block。最接近的 HTML 元素是<table>,尽管默认情况下 HTML 表格不是内联的。

table-row

指定元素是表格单元格行。对应的 HTML 元素是<tr>

table-row-group

指定元素将一个或多个表格行分组。对应的 HTML 值是<tbody>

table-header-group

table-row-group非常相似,只是在视觉格式化上,页眉行组始终显示在所有其他行和行组之前,并在任何顶部标题之后显示。在打印时,如果表格需要多页打印,用户代理可能会在每页顶部重复显示页眉行(例如 Firefox 就是这样做的)。规范没有定义如果将table-header-group分配给多个元素会发生什么。页眉组可以包含多行。HTML 等效元素是<thead>

table-footer-group

table-header-group非常相似,只是页脚行组始终显示在所有其他行和行组之后,并且在任何底部标题之前显示。在打印时,如果表格需要多页打印,用户代理可能会在每页底部重复显示页脚行。规范没有定义如果将table-footer-group分配给多个元素会发生什么。这相当于 HTML 元素<tfoot>

table-column

描述表格单元格的列。在 CSS 术语中,具有此display值的元素在视觉上不会被渲染,就像它们具有none值一样。它们的存在主要是为了帮助定义列内单元格的显示。HTML 等效元素是<col>

table-column-group

分组一个或多个列。像table-column元素一样,table-column-group元素不会被渲染,但这个值对于定义列组内元素的显示方式非常有用。HTML 等效元素是<colgroup>

table-cell

表示表格中的单元格。HTML 元素<th><td>都是table-cell元素的示例。

table-caption

定义表格的标题。CSS 没有定义多个元素具有 caption 值时应该发生什么,但明确警告,“作者不应将多个带有 display: caption 的元素放在表格或内联表元素内部。”

你可以通过摘录 CSS 2.1 规范附录 D 中提供的 HTML 4.0 样式表示例来快速总结这些值的一般效果:

table {display: table;}
tr {display: table-row;}
thead {display: table-header-group;}
tbody {display: table-row-group;}
tfoot {display: table-footer-group;}
col {display: table-column;}
colgroup {display: table-column-group;}
td, th {display: table-cell;}
caption {display: table-caption;}

在 XML 中,默认情况下元素没有显示语义,因此这些值变得非常有用。考虑以下标记:

<scores>
    <headers>
        <label>Team</label>
        <label>Score</label>
    </headers>
    <game sport="MLB" league="NL">
        <team>
            <name>Reds</name>
            <score>8</score>
        </team>
        <team>
            <name>Cubs</name>
            <score>5</score>
        </team>
    </game>
</scores>

可以通过以下样式以表格形式进行格式化:

scores {display: table;}
headers {display: table-header-group;}
game {display: table-row-group;}
team {display: table-row;}
label, name, score {display: table-cell;}

然后可以根据需要对各个单元格进行样式设置,例如将 <label> 元素设置为粗体并右对齐 <score>

行优先

CSS 将其表格模型定义为行优先。该模型假设作者将使用显式声明行的标记语言。另一方面,列是从单元格行的布局派生出来的。因此,第一列由每行的第一个单元格组成;第二列由第二个单元格组成,依此类推。

在 HTML 中,行优先并不是一个主要问题,因为标记语言已经是面向行的。在 XML 中,行优先影响更大,因为它限制了作者可以定义表格标记的方式。由于 CSS 表格模型的行向性特性,基于列布局的标记语言实际上并不可行(假设意图是使用 CSS 来呈现这些文档)。

尽管 CSS 表格模型是面向行的,但列仍然在布局中发挥作用。一个单元格可以属于两个上下文(行和列),即使它是从文档源中的行元素派生而来。然而,在 CSS 中,列和列组只能接受四个非表属性:borderbackgroundwidthvisibility

此外,这四个属性中的每一个在列上下文中都有特殊规则:

border

只有当 border-collapse 属性值为 collapse 时,才能为列和列组设置边框。在这种情况下,列和列组边框参与折叠算法,该算法在每个单元格边缘设置边框样式。(见 “折叠单元格边框”。)

background

列或列组的背景只在单元格和其行具有透明背景的情况下可见。(见 “处理表格层”。)

width

width 属性定义了列或列组的最小宽度。列(或组)内的单元格内容可能会强制列变宽。

visibility

如果某一列或列组的visibility值为collapse,则不会渲染该列(或组)中的任何单元格。跨越折叠列到其他列的单元格会被剪切,以及跨越其他列到折叠列的单元格。此外,表格的总宽度将减少该列本应占用的宽度。对于列或列组的任何visibility值声明,除collapse外的值都会被忽略。

插入匿名表对象

标记语言可能不包含足够的元素来完全表示 CSS 中定义的表格,或者作者可能会忘记包含所有必要的元素。例如,请考虑以下 HTML:

<table>
    <td>Shirt size:</td>
    <td><select> … </select></td>
</table>

你可能会看一眼这个标记,认为它定义了一个单行的两个单元格表格,但在结构上,没有定义行的元素(因为缺少了<tr>)。

为了涵盖这些可能性,CSS 定义了一种机制,用于插入“缺失”的表组件作为匿名对象。以一个基本示例说明其工作原理,让我们重新看一下我们的缺失行 HTML 示例。从 CSS 角度来看,实际发生的是在<table>元素和其后代表格单元格之间插入了一个匿名 table-row 对象:

<table>
  <!--anonymous table-row object begins-->
    <td>Name:</td>
    <td><input type="text"></td>
  <!--anonymous table-row object ends-->
</table>

图 13-2 显示了这一过程的视觉表现。虚线代表插入的匿名表行。

图片

图 13-2. 表格格式中的匿名对象生成

CSS 表模型中可能会发生七种匿名对象插入。这七条规则,像继承和特异性一样,是试图在 CSS 行为方式上赋予直观意义的机制的一个例子。

规则如下:

  1. 如果一个table-cell元素的父元素不是table-row元素,则在table-cell元素和其父元素之间插入一个匿名table-row对象。插入的对象将包括table-cell元素的所有连续兄弟元素。

    即使父元素是table-row-group,情况也是如此。例如,假设以下 CSS 应用于 XML 之后:

    system {display: table;}
    planet {display: table-row-group;}
    name, moons {display: table-cell;}
    
    <system>
        <planet>
            <name>Mercury</name>
            <moons>0</moons>
        </planet>
        <planet>
            <name>Venus</name>
            <moons>0</moons>
        </planet>
    </system>
    

    这两组单元格都将被封装在一个插入的匿名table-row对象中,该对象位于它们和<planet>元素之间。

  2. 如果一个table-row元素的父元素不是tableinline-tabletable-row-group元素,则在table-row元素和其父元素之间插入一个匿名table元素。插入的对象将包括table-row元素的所有连续兄弟元素。考虑以下样式和标记:

    docbody {display: block;}
    planet {display: table-row;}
    
    <docbody>
        <planet>
            <name>Mercury</name>
            <moons>0</moons>
        </planet>
        <planet>
            <name>Venus</name>
            <moons>0</moons>
        </planet>
    </docbody>
    

    因为<planet>元素的父元素的display值为block,所以在<planet>元素和<docbody>元素之间插入了一个匿名table对象。这个匿名table对象将包裹两个连续的<planet>元素。

  3. 如果table-column元素的父元素不是tableinline-tabletable-column-group元素,则在table-column元素和其父元素之间插入一个匿名的table元素。这与刚讨论的table-row规则类似,只是针对其列导向的特性。

  4. 如果table-row-grouptable-header-grouptable-footer-grouptable-column-grouptable-caption元素的父元素不是table元素,则在元素和其父元素之间插入一个匿名的table对象。

  5. 如果tableinline-table元素的子元素不是table-row-grouptable-header-grouptable-footer-grouptable-rowtable-caption元素,则在table元素和其子元素之间插入一个匿名的table-row对象。此匿名对象跨越子元素之后的所有连续兄弟元素,这些兄弟元素本身不是table-row-grouptable-header-grouptable-footer-grouptable-rowtable-caption元素。考虑以下标记和样式:

    system {display: table;}
    planet {display: table-row;}
    name, moons {display: table-cell;}
    
    <system>
        <planet>
            <name>Mercury</name>
            <moons>0</moons>
        </planet>
        <name>Venus</name>
        <moons>0</moons>
    </system>
    

    在这里,一个单独的匿名的table-row对象将被插入在<system>元素和第二组<name><moons>元素之间。<planet>元素不被匿名对象包围,因为其displaytable-row

  6. 如果table-row-grouptable-header-grouptable-footer-group元素的子元素不是table-row元素,则在元素和其子元素之间插入一个匿名的table-row对象。此匿名对象跨越子元素之后的所有连续兄弟元素,这些兄弟元素本身不是table-row对象。考虑以下标记和样式:

    system {display: table;}
    planet {display: table-row-group;}
    name, moons {display: table-cell;}
    
    <system>
        <planet>
            <name>Mercury</name>
            <moons>0</moons>
        </planet>
        <name>Venus</name>
        <moons>0</moons>
    </system>
    

    在这种情况下,每组<name><moons>元素将被匿名的table-row元素包围。对于第二组,插入遵循规则 5。对于第一组,匿名对象插入在<planet>元素和其子元素之间,因为<planet>元素是一个table-row-group元素。

  7. 如果table-row元素的子元素不是table-cell元素,则在元素和其子元素之间插入一个匿名的table-cell对象。此匿名对象包围子元素之后的所有连续兄弟元素,这些兄弟元素本身不是table-cell元素。考虑以下标记和样式:

    system {display: table;}
    planet {display: table-row;}
    name, moons {display: table-cell;}
    
    <system>
        <planet>
            <name>Mercury</name>
            <num>0</num>
        </planet>
    </system>
    

    因为元素<num>没有与表格相关的display值,所以在<planet>元素和<num>元素之间插入一个匿名的table-cell对象。

    此行为还适用于匿名内联框的封装。假设未包括<num>元素:

    <system>
        <planet>
            <name>Mercury</name>
            0
        </planet>
    </system>
    

    0仍将被匿名的table-cell对象包围。为了进一步说明这一点,这里有一个从 CSS 规范调整的示例:

    example {display: table-cell;}
    row {display: table-row;}
    hey {font-weight: 900;}
    
    <example>
        <row>This is the <hey>top</hey> row.</row>
        <row>This is the <hey>bottom</hey> row.</row>
    </example>
    

    在每个<row>元素内,文本片段和hey元素都被匿名的table-cell对象包围。

处理表格层次

对于表格呈现的组装,CSS 定义了六个单独的,用于放置表格各个方面的内容。图 13-3 展示了这些层。

image

图 13-3. 表格呈现中使用的格式化层

基本上,表格的每个方面的样式都在它们各自的层上绘制。因此,如果 <table> 元素具有绿色背景和 1 像素黑色边框,这些样式将绘制在最低层上。列组的任何样式将绘制在其上一层,列本身在再上一层,依此类推。对应表格单元格的顶层最后绘制。

大多数情况下,这是一个逻辑过程;毕竟,如果为表格单元格声明背景颜色,则希望它覆盖表格元素的背景。图 13-3 揭示的最重要的一点是列样式位于行样式之下,因此行的背景将覆盖列的背景。

需要记住,默认情况下,所有元素都具有透明背景。因此,在以下标记中,表格元素的背景将“透过”没有自己背景的单元格、行、列等可见,正如图 13-4 中所示。

<table style="background: #B84;">
    <tr>
        <td>hey</td>
        <td style="background: #ABC;">there</td>
    </tr>
    <tr>
        <td>what’s</td>
        <td>up?</td>
    </tr>
    <tr style="background: #CBA;">
        <td>not</td>
        <td style="background: #ECC;">much</td>
    </tr>
</table>

css5 1304

图 13-4. 透过其他层看表格格式化层的背景

使用标题

表格标题通常是描述表格内容性质的简短文本。例如,2026 年第四季度股票行情表可能有一个标题元素,内容为“2026 年 Q4 股票表现”。通过 caption-side 属性,您可以将此元素放置在表格上方或下方,无论标题在表格结构中的位置如何。(在 HTML5 中,<caption> 元素只能作为 <table> 元素的第一个子元素出现,但其他语言可能有不同的规则。)

在视觉上,标题有点奇怪。CSS 规范规定,标题的格式化方式就像是一个块级框,放置在表格框的前(或后),但有一个例外:标题仍然可以从表格中继承值。

一个简单的例子足以说明标题展示的大部分重要方面。考虑以下示例,如图 13-5 所示:

table {color: white; background: #840; margin: 0.5em 0;}
caption {background: #B84; margin: 1em 0;}
table.one caption {caption-side: top;}
table.two caption {caption-side: bottom;}
td {padding: 0.5em;}

每个 <caption> 元素中的文本从表格中继承了 colorwhite,而标题则有自己的背景。每个表格的外边框与标题的外边距之间的分隔距离为 1 em,因为表格和标题的边距已经合并。最后,标题的宽度基于 <table> 元素的内容宽度,该元素被视为标题的包含块。

css5 1305

图 13-5. 样式化标题和表格

大部分情况下,标题的样式与任何块级元素一样:它们可以有内边距、边框、背景等等。例如,如果我们需要改变标题内文本的水平对齐方式,我们使用属性 text-align。因此,要右对齐前面示例中的标题,我们会这样写:

caption {background: gray; margin: 1em 0;
    caption-side: top; text-align: right;}

表格单元格边框

CSS 有两种截然不同的表格边框模型。分离边框模型 在单元格在布局上相互分隔时生效。合并边框模型 单元格之间没有视觉上的分隔,单元格边框相互合并或合并为一个。前者是默认模型,但你可以使用属性 border-collapse 在这两种模型之间进行选择。

此属性的全部意义在于提供一种确定用户代理将采用哪种边框模型的方法。如果值为 collapse,则使用合并边框模型。如果值为 separate,则使用分离边框模型。我们首先查看后者,因为它更容易描述,也是默认的。

分离单元格边框

在分离边框模型中,表格中的每个单元格都与其他单元格相隔一定距离,单元格的边框不会合并在一起。因此,给定以下样式和标记,你应该得到图 13-6 中显示的结果。

td {border: 3px double black; padding: 3px;}
tr:nth-child(2) td:nth-child(2) {border-color: gray;}
<table cellspacing="0">
    <tr>
        <td>cell one</td>
        <td>cell two</td>
    </tr>
    <tr>
        <td>cell three</td>
        <td>cell four</td>
    </tr>
</table>

注意,单元格边框接触但保持各自独立。单元格之间的三条线实际上是两个并排的双边框;第四个单元格周围的灰色边框有助于更清楚地显示这一点。

image

图 13-6. 分离(因此是分开的)单元格边框

在前面的示例中包含 HTML 属性 cellspacing,是为了确保单元格之间没有间隔,但它的存在可能会有些令人困扰。毕竟,如果你可以将边框定义为分开,应该有办法使用 CSS 来改变单元格之间的间距。幸运的是,有办法实现。

应用边框间距

一旦你将表格单元格的边框分开,你可能希望这些边框之间相隔一定的距离。这可以通过属性 border-spacing 来轻松实现,它为 HTML 属性 cellspacing 提供了更强大的替代方案。

此属性的值可以给出一个或两个长度。如果你希望所有单元格之间相隔一个像素,border-spacing: 1px; 就足够了。另一方面,如果你希望单元格之间的水平间距为 1 像素,垂直间距为 5 像素,写为 border-spacing: 1px 5px;。如果给出两个长度,第一个始终是水平间距,第二个始终是垂直间距。

这些间距值也适用于表格外部的单元格边框与 table 元素本身的填充之间。根据以下样式,您将获得类似于 图 13-7 所示的结果:

table {border-collapse: separate; border-spacing: 5px 8px;
padding: 12px; border: 2px solid black;}
td { border: 1px solid gray;}
td#squeeze {border-width: 5px;}

图 13-7 显示了任意两个水平相邻单元格边框之间的 5 像素空间,以及最右和最左单元格与 <table> 元素的右边和左边边框之间的 17 像素空间。同样,垂直相邻单元格的边框相距 8 像素,顶部和底部行的单元格边框分别距离表格的顶部和底部边框 20 像素。无论单元格本身的边框宽度如何,表格内部单元格边框的分隔都是恒定的。

还要注意,border-spacing 的声明是在表格本身上进行的,而不是在单个单元格上。如果在前面的示例中为 <td> 元素声明了 border-spacing,则会被忽略。

css5 1307

图 13-7. 单元格和其封闭表格之间的边框间距效果

在分隔边框模型中,无法为行、行组、列和列组设置边框。对于这些元素声明的任何边框属性,必须被 CSS 兼容的用户代理忽略。

处理空单元格

从视觉上讲,每个单元格在表格中都是独立的,那么对于空单元格(即没有内容的单元格)怎么处理?您有两种选择,这两种选择反映在 empty-cells 属性的值中。

如果 empty-cells 设置为 show,则空单元格的边框和背景将被绘制,就像具有内容的表格单元格一样。如果值为 hide,则不会绘制单元格的任何部分,就像单元格设置为 visibility: hidden 一样。

如果单元格包含任何内容,则不能视为空单元格。在这种情况下,“内容”不仅包括文本、图像、表单元素等,还包括非断行空格实体 (&nbsp;) 和除回车 (CR)、换行 (LF)、制表符和空格字符之外的任何空白字符。如果一行中的所有单元格都为空,并且所有单元格的 empty-cells 值为 hide,则整行将被视为行元素设置为 display: none

折叠单元格边框

当折叠边框模型大部分描述 HTML 表格没有任何单元格间距时,它比分隔边框模型要复杂得多。以下规则将折叠单元格边框与分隔边框模型区分开来:

  • 元素的 display 属性为 tableinline-table 时,在 border-collapsecollapse 时不能有任何填充,尽管可以有边距。因此,在折叠边框模型中,表格外边框和其最外层单元格的边缘之间永远不会发生分隔。

  • 边框可以应用于单元格、行、行组、列和列组。表格本身像往常一样可以有一个边框。

  • 在折叠边框模型中,单元格边框之间从不存在分隔。事实上,边框会在相接处合并,只有一个折叠的边框实际上被绘制出来。这有点类似于边距合并,最大的边距会胜出。当单元格边框折叠时,“最有趣”的边框会胜出。

  • 一旦它们被折叠,单元格之间的边框会居中于假设的单元格之间的网格线上。

我们将在接下来的两节中更详细地探讨最后两点。

折叠边框布局

为了更好地理解折叠边框模型的工作原理,让我们看一下单个表行的布局,如 图 13-8 所示。

每个单元格的填充和内容宽度都在边框内部,如预期那样。对于单元格之间的边框,边框的一半位于两个单元格网格线的一侧,另一半位于另一侧。在每种情况下,每个单元格边缘只绘制一个边框。您可能会认为每个单元格的边框的一半会绘制在网格线的两侧,但事实并非如此。

image

图 13-8. 使用折叠边框模型布局的表行的布局

例如,假设中间单元格上的实线边框是绿色的,外部两个单元格上的实线边框是红色的。中间单元格的右侧和左侧边框(与相邻单元格的边框合并)将全部是绿色的或全部是红色的,具体取决于哪个边框占优势。我们将在下一节讨论如何判断哪个边框占优势。

您可能已经注意到外部边框超出了表格的宽度。这是因为在这种模型中,表格边框的一半包括在宽度内,另一半突出超出该距离,位于边距本身。这可能看起来有点奇怪,但这就是模型定义的工作方式。

规范中包含了一个布局公式,这里再次重现,以供喜欢这些内容的人参考:

行宽度 = (0.5 × border-width-0) + padding-left-1 + width-1 + padding-right-1 + border-width-1 + padding-left-2 +...+ padding-right-n + (0.5 × border-width-n)

每个 border-width-n 是指第 n 个单元格和下一个单元格之间的边框;因此,border-width-3 是指第三个和第四个单元格之间的边框。值 n 表示行中的总单元格数。

这种机制有一个小小的例外。在开始合并边框表的布局时,用户代理计算表本身的初始左右边框。它通过检查表的第一行第一个单元格的左边框,并将该边框宽度的一半作为表的初始左边框宽度来完成这一点。然后,用户代理检查表的第一行最后一个单元格的右边框,并使用该宽度的一半来设置表的初始右边框宽度。对于第一行之后的任何行,如果左边框或右边框比初始边框宽度更宽,则会突出到表的边距区域。

如果边框是奇数显示元素(像素、打印点等)宽度,则用户代理会决定如何在网格线上居中边框。用户代理可以将边框稍微偏离中心,四舍五入到偶数显示元素,使用反锯齿,或者调整任何其他合理的方法。

边框合并

当两个或更多边框相邻时,它们会合并在一起。事实上,它们并不是真正合并,而是争夺谁将在其他边框上占主导地位。严格的规则决定了哪些边框将胜出,哪些不会:

  • 如果合并的边框中有一个border-stylehidden,则它优先于所有其他合并的边框。在此位置的所有边框都将隐藏。

  • 如果所有边框都是可见的,则更宽的边框优先于较窄的边框。因此,如果一个 2 像素的点状边框和一个 5 像素的双重边框合并,则该位置的边框将是一个 5 像素的双重边框。

  • 如果所有合并的边框具有相同的宽度但不同的边框样式,则按照以下顺序,从最优到最不优先:doublesoliddasheddottedridgeoutsetgrooveinsetnone。因此,如果两个具有相同宽度的边框合并,一个是dashed,另一个是outset,则该位置的边框将为dashed

  • 如果合并的边框具有相同的样式和宽度,但颜色不同,则所使用的颜色按照以下列表中元素的优先级,从最优到最不优:单元格,行,行组,列,列组,表。因此,如果单元格和列的边框(在除颜色之外完全相同的情况下)合并,则使用单元格的边框颜色(以及样式和宽度)。如果合并的边框来自于同一类型的元素,例如两个具有相同样式和宽度但颜色不同的行边框,则颜色来自于更接近元素块起始和行内起始边缘的边框。

下面的样式和标记,在图 13-9 中呈现,帮助说明四条规则:

table {border-collapse: collapse;
border: 3px outset gray;}
td {border: 1px solid gray; padding: 0.5em;}
#r2c1, #r2c2 {border-style: hidden;}
#r1c1, #r1c4 {border-width: 5px;}
#r2c4 {border-style: double; border-width: 3px;}
#r3c4 {border-style: dotted; border-width: 2px;}
#r4c1 {border-bottom-style: hidden;}
#r4c3 {border-top: 13px solid silver;}
<table>
    <tr>
        <td id="r1c1">1-1</td>
        <td id="r1c2">1-2</td>
        <td id="r1c3">1-3</td>
        <td id="r1c4">1-4</td>
    </tr>
    <tr>
        <td id="r2c1">2-1</td>
        <td id="r2c2">2-2</td>
        <td id="r2c3">2-3</td>
        <td id="r2c4">2-4</td>
    </tr>
    <tr>
        <td id="r3c1">3-1</td>
        <td id="r3c2">3-2</td>
        <td id="r3c3">3-3</td>
        <td id="r3c4">3-4</td>
    </tr>
    <tr>
        <td id="r4c1">4-1</td>
        <td id="r4c2">4-2</td>
        <td id="r4c3">4-3</td>
        <td id="r4c4">4-4</td>
    </tr>
</table>

css5 1309

图 13-9. 操纵边框宽度、样式和颜色会导致一些不寻常的结果

让我们逐个考虑每个单元格发生的情况:

  • 对于单元格 1-1 和 1-4,它们的 5 像素边框比其相邻边框更宽,因此它们不仅胜过相邻单元格的边框,还胜过表格本身的边框。唯一的例外是单元格 1-1 的底部,它被抑制了。

  • 单元格 1-1 的底部边框被抑制,因为单元格 2-1 和 2-2 明确隐藏了它们的边框,完全删除了单元格边缘的任何边框。同样,表格的边框(在单元格 2-1 的左边缘)输给了单元格的边框。单元格 4-1 的底部边框也被隐藏了,因此阻止了任何边框出现在单元格下方。

  • 单元格 2-4 的 3 像素双边框被单元格 1-4 的 5 像素实线边框覆盖。单元格 2-4 的边框又覆盖了它与单元格 2-3 之间的边框,因为它既更宽又更“有趣”。单元格 2-4 还覆盖了它与单元格 3-4 之间的边框,即使它们的宽度相同,因为 2-4 的双线样式被定义为比 3-4 的点线边框“更有趣”。

  • 单元格 3-3 的 13 像素底部银色边框不仅覆盖了单元格 4-3 的顶部边框,而且还影响了包含这两个单元格及其行内内容的布局。表。

  • 对于沿表格外边缘的未特别样式化的单元格,它们的 1 像素实线边框被表格元素本身的 3 像素凸出边框所覆盖。

事实上,这确实和听起来的一样复杂,尽管行为大部分是直观的,并且随着实践会显得更加合理。值得注意的是,基本的 Netscape 1.1 时代的表格呈现可以通过一组相当简单的规则来捕捉:

table {border-collapse: collapse; border: 2px outset gray;}
td {border: 1px inset gray;}

是的,当表格首次推出时,默认情况下确实使其看起来有 3D 效果。那时候是一个不同的时代。

表格尺寸调整

现在我们深入了解表格格式和单元格边框外观的内部构造,你已经掌握了理解表格及其内部元素尺寸的基本要素。在确定表格宽度时,CSS 有两种方法:固定宽度布局自动宽度布局 。表格高度会根据需要自动计算,无论使用何种宽度算法。

宽度

由于有两种方法可以确定表格的宽度,因此对于给定的表格可以声明使用哪种方法。您可以使用属性table-layout来选择两种表格宽度计算方法之间的区别。

尽管两种模型在布置给定表格时可能会有不同的结果,但两者之间根本的区别在于速度。采用固定宽度表格布局,用户代理可以比自动宽度模型更快速地计算表格的布局。

固定布局

固定布局模型之所以如此快速的主要原因在于,其布局并不完全依赖于表格单元格的内容。相反,它由表格的宽度值、其列元素以及该表格内第一行的单元格驱动。

固定布局模型按以下步骤工作:

  • 任何具有除auto以外的值的width属性的列元素都会设置该整列的宽度。

    • 如果列具有auto宽度,但表格的第一行单元格在该列内具有除auto以外的width,则单元格设置该整列的宽度。如果单元格跨多列,则宽度在列之间分配。

    • 仍然自动调整大小的任何列将被调整为宽度尽可能相等。

在这一点上,表格的宽度被设置为表格的width值或列宽度之和中较大的值。如果表格的宽度比其列宽度大,差值将被除以列数,然后将结果添加到每一列。

这种方法很快,因为所有列宽度都是由表格的第一行定义的。在第一行之后的任何行中,根据第一行定义的列宽度来调整列的大小。在这些后续行中的单元格不会——实际上也不能——更改列宽度,这意味着对这些单元格分配的任何width值都将被忽略。如果单元格的内容不适合其单元格,那么单元格内容是否被裁剪、可见或生成滚动条由单元格的overflow值决定。

让我们考虑以下样式和标记,这些在图 13-10 中有所说明:

table {table-layout: fixed; width: 400px;
    border-collapse: collapse;}
td {border: 1px solid;}
col#c1 {width: 200px;}
#r1c2 {width: 75px;}
#r2c3 {width: 500px;}
<table>
    <colgroup> <col id="c1"><col id="c2"><col id="c3"><col id="c4"> </colgroup>
    <tr>
        <td id="r1c1">1-1</td>
        <td id="r1c2">1-2</td>
        <td id="r1c3">1-3</td>
        <td id="r1c4">1-4</td>
    </tr>
    <tr>
	   <td id="r2c1">2-1</td>
	   <td id="r2c2">2-2</td>
	   <td id="r2c3">2-3</td>
	   <td id="r2c4">2-4</td>
    </tr>
	(…more rows here…)
</table>

image

图 13-10. 固定宽度表格布局

第一列宽度为 200 像素,恰好是表格宽度 400 像素的一半。第二列宽度为 75 像素,因为该列内第一行的单元格已被分配了显式宽度。第三和第四列各 61 像素宽。为什么?因为第一和第二列的列宽之和(275 像素),加上列之间的各种边框(3 像素),等于 278 像素。然后,400 减去 278 等于 122,将其一分为二得到 61,这就是第三和第四列的宽度。#r2c3的 500 像素宽度怎么办?因为该单元格不在表格的第一行,所以被忽略。

注意,表格不需要具有显式的width值才能使用固定宽度布局模型,尽管这确实有所帮助。例如,给定以下情况,用户代理可以计算表格的宽度,比父元素的宽度窄 50 像素。然后它会在固定布局算法中使用这个计算出的宽度:

table {table-layout: fixed; margin: 0 25px; width: auto;}

这并非必须。用户代理还可以使用auto值的width来布置任何表格,通过自动宽度布局模型。

自动布局

自动宽度布局模型虽然不如固定布局快,但可能更为熟悉,因为这基本上是自 HTML 表格诞生以来就采用的模型。在大多数现代浏览器中,只要表格具有widthauto,无论table-layout的值如何,都会触发使用该模型,尽管这并不是一定的。

自动布局较慢的原因在于,表格必须等到用户代理程序查看了表格中的所有内容后才能布局。用户代理必须以考虑每个单元格的内容和样式的方式布局整个表格。通常,这需要用户代理进行一些计算,然后再次回到表格执行第二轮计算(如果不止一轮)。

因为与 HTML 表格类似,表格的布局依赖于所有单元格中的内容,所以必须完全检查内容。如果最后一行中的一个 400 像素宽的图像放在一个单元格中,那么该内容将迫使该列中所有上方的单元格(即同一列中的单元格)至少有 400 像素宽。因此,必须计算每个单元格的宽度,并进行调整(可能触发另一轮内容宽度计算)之后,才能布局表格。

模型的详细信息可以用以下步骤表示:

  1. 对于列中的每个单元格,计算最小和最大单元格宽度。

    1. 确定显示内容所需的最小宽度。在确定这种最小内容宽度时,内容可以流动到任意数量的行,但不得突出单元格框。如果单元格具有大于最小可能宽度的width值,则最小单元格宽度设为width的值。如果单元格的width值为auto,则最小单元格宽度设为最小内容宽度。

    2. 对于最大宽度,确定显示内容所需的宽度,除了由显式换行(例如<br>元素)强制的换行外,不应有任何其他换行。该值即为最大单元格宽度。

  2. 对于每一列,计算最小和最大列宽度。

    1. 列的最小宽度由列中的单元格的最大最小宽度确定。如果列已经被给定一个明确的width值,且该值大于列内任何最小单元格宽度,则最小列宽度设为width的值。

    2. 对于最大宽度,取列中单元格的最大最大宽度。如果列已经给定一个明确的width值,且该值大于列内任何最大单元格宽度,则最大列宽度设为width的值。这两种行为重新创建了强制将任何列扩展为其最宽单元格的传统 HTML 表格行为。

  3. 如果一个单元格跨越多列,那么最小列宽度的总和必须等于跨列单元格的最小宽度。同样地,最大列宽度的总和必须等于跨列单元格的最大宽度。用户代理应当将列宽度的任何变化均匀分配到跨度的各列中。

此外,用户代理必须考虑到,当列的宽度设定为百分比值时,这个百分比是相对于表格的宽度来计算的——即使用户代理此时还不知道表格的实际宽度!它必须暂存这个百分比值,并在算法的下一部分中使用它。

在此时,用户代理已经计算出每列的宽度范围。有了这些信息,它接下来可以真正地计算表格的宽度。具体过程如下:

  1. 如果表格的计算宽度不是auto,则将计算表格宽度与所有列宽度的总和以及任何边框和单元格间距进行比较。(百分比宽度的列可能在此时计算。)两者中较大的值将成为表格的最终宽度。如果表格的计算宽度大于列宽度、边框和单元格间距的总和,则差值将被除以列数,结果将添加到每一列中。

  2. 如果表格的计算宽度是auto,则表格的最终宽度由列宽度、边框和单元格间距的总和决定。这意味着表格的宽度将仅适合显示其内容,就像传统的 HTML 表格一样。任何百分比宽度的列将使用该百分比作为约束条件——但用户代理不必满足这一约束。

只有完成了最后一步,用户代理才能实际布局表格。

下面的样式和标记,如图 13-11 所示,有助于说明这一过程的工作原理:

table {table-layout: auto; width: auto;
    border-collapse: collapse;}
td {border: 1px solid; padding: 0;}
col#c3 {width: 25%;}
#r1c2 {width: 40%;}
#r2c2 {width: 50px;}
#r2c3 {width: 35px;}
#r4c1 {width: 100px;}
#r4c4 {width: 1px;}
<table>
    <colgroup> <col id="c1"><col id="c2"><col id="c3"><col id="c4"> </colgroup>
    <tr>
        <td id="r1c1">1-1</td>
        <td id="r1c2">1-2</td>
        <td id="r1c3">1-3</td>
        <td id="r1c4">1-4</td>
    </tr>
    <tr>
        <td id="r2c1">2-1</td>
        <td id="r2c2">2-2</td>
        <td id="r2c3">2-3</td>
        <td id="r2c4">2-4</td>
    </tr>
    <tr>
        <td id="r3c1">3-1</td>
        <td id="r3c2">3-2</td>
        <td id="r3c3">3-3</td>
        <td id="r3c4">3-4</td>
    </tr>
    <tr>
        <td id="r4c1">4-1</td>
        <td id="r4c2">4-2</td>
        <td id="r4c3">4-3</td>
        <td id="r4c4">4-4</td>
    </tr>
</table>

image

图 13-11. 自动表格布局

让我们依次考虑每一列发生的情况:

  • 对于第一列,唯一显式设置的单元格或列宽度是 4-1 单元格,宽度为100px。由于内容较短,最小和最大列宽度均设置为100px。(如果列中的单元格包含多个句子的文本,则最大列宽度将增加到足以显示所有文本而不换行的宽度。)

  • 对于第二列,声明了两个width:单元格 1-2 的宽度为40%,而单元格 2-2 的宽度为50px。这一列的最小宽度为50px,最大宽度为最终表格宽度的40%

  • 对于第三列,只有第 3-3 单元格有明确的宽度(35px),但是列本身被赋予了width25%。因此,最小列宽度为 35 像素,最大宽度为最终表格宽度的 25%。

  • 对于第四列,只有单元格 4-4 给定了显式宽度(1px)。这小于最小内容宽度,因此最小和最大列宽度均等于单元格的最小内容宽度。这结果为计算的 22 像素,因此最小和最大宽度都是 22 像素。

现在用户代理知道四列的最小和最大宽度如下:

  1. 最小值为 100 像素,最大值为 100 像素。

  2. 最小值为 50 像素,最大值为 40%。

  3. 最小值为 35 像素,最大值为 25%。

  4. 最小值为 22 像素,最大值为 22 像素。

表格的最小宽度是所有列最小宽度的总和,加上列之间折叠的边框,总共为 215 像素。表格的最大宽度是123px + 65%,其中123px来自第一列和最后一列及其折叠边框的份额。这个最大值计算结果为 351.42857142857143 像素(给定123px代表总表格宽度的 35%)。有了这个数值,第二列将宽度为 140.5 像素,第三列将宽度为 87.8 像素。这些可能会被用户代理四舍五入为整数,如141px88px,也可能根据精确的渲染方法而定(这些是 Figure 13-11 中使用的数字)。

用户代理不需要实际使用最大值;它们可以选择其他操作。

这(尽管可能不像是)是一个相对简单和直接的例子:所有内容基本上是相同宽度,大部分声明的宽度都是像素长度。如果表格包含图像、文字段落、表单元素等内容,那么确定表格布局的过程可能会复杂得多。

高度

在计算表格宽度的所有努力之后,你可能会想知道高度计算会更加复杂。在 CSS 方面,实际上很简单,尽管浏览器开发者可能不这么认为。

最容易描述的情况是通过height属性明确设置表格高度。在这种情况下,表格的高度由height的值定义。这意味着表格的高度可能比其行高的总和要高或者低。请注意,对于表格,height更像是min-height,因此如果定义的height值小于行高的总和,它可能被忽略。

相比之下,如果表格的height值大于其行高的总和,则规范明确拒绝定义应发生的情况,而是指出此问题可能会在 CSS 的未来版本中解决。用户代理可以展开表格的行以填充其高度,或在表格框中留白,或完全不同的其他操作。这取决于每个用户代理的决定。

注意

截至 2022 年中,用户代理的最常见行为是增加表格行的高度,以填充其整体高度。这是通过取表格高度与行高总和的差异,除以行数,并将结果应用到每一行来实现的。

如果表格的heightauto,其高度是表格内所有行的高度总和,加上任何边框和单元格间距。为了确定每行的高度,用户代理通过类似于确定列宽度的过程进行:它计算每个单元格内容的最小和最大高度,然后用这些值来推导出每行的最小和最大高度。在为所有行完成这一过程后,用户代理确定每行的高度,将它们依次堆叠,并使用总和来确定表格的高度。

除了如何处理具有显式高度的表格及其内部的行高之外,您还可以将以下内容添加到 CSS 未定义的事物列表中:

  • 百分比高度对表格单元格的影响

  • 百分比高度对表格行和行组的影响

  • 跨行单元格如何影响跨越的行的高度,除非行必须包含跨越的单元格

如您所见,在表格中的高度计算很大程度上取决于用户代理的处理方式。历史证据表明,这将导致每个用户代理采取不同的方法,因此您应尽可能避免设置表格高度。

对齐

有趣的是,与单元格和行高相比,单元格内内容的对齐更加明确定义。即使对于垂直对齐,这也很容易影响行的高度。

水平对齐是最简单的。要在单元格中对齐内容,您可以使用text-align属性。实际上,该单元格被视为块级框,并且其中的所有内容都根据text-align值对齐。

要在表格单元格中垂直对齐内容,vertical-align是相关属性。它使用许多用于垂直对齐内联内容的相同值,但是当应用于表格单元格时,这些值的含义会发生变化。简要总结三种最简单的情况:

top

单元格内容的顶部与其行的顶部对齐;在跨行单元格的情况下,单元格内容的顶部与其跨越的第一行的顶部对齐。

bottom

单元格内容的底部与其行的底部对齐;在跨行单元格的情况下,单元格内容的底部与其跨越的最后一行的底部对齐。

middle

单元格内容的中部与其行的中部对齐;在跨行单元格的情况下,单元格内容的中部与其跨越的所有行的中部对齐。

这些都在图 13-12 中进行了说明,该图使用以下样式和标记:

table {table-layout: auto; width: 20em;
border-collapse: separate; border-spacing: 3px;}
td {border: 1px solid; background: silver;
    padding: 0;}
div {border: 1px dashed gray; background: white;}
#r1c1 {vertical-align: top; height: 10em;}
#r1c2 {vertical-align: middle;}
#r1c3 {vertical-align: bottom;}
<table>
  <tr>
    <td id="r1c1">
      <div>The contents of this cell are top-aligned.</div>
    </td>
    <td id="r1c2">
      <div>The contents of this cell are middle-aligned.</div>
    </td>
    <td id="r1c3">
      <div>The contents of this cell are bottom-aligned.</div>
    </td>
  </tr>
</table>

css5 1312

图 13-12. 单元格内容的垂直对齐

在每种情况下,通过自动增加单元格本身的填充来实现所需的对齐效果。在图 13-12 的第一个单元格中,单元格的底部填充已经更改为等于单元格框的高度与单元格内内容高度之间的差异。对于第二个单元格,单元格的顶部和底部填充已被重置为相等,从而垂直居中单元格内容。在最后一个单元格中,单元格的顶部填充已被修改。

第四种可能的对齐值是 baseline,比前三种稍微复杂一些:

baseline

单元格的基线与其行的基线对齐;对于跨行的单元格,单元格的基线与其跨越的第一行的基线对齐。

最好提供一个插图(图 13-13),然后讨论发生的情况。

image

图 13-13. 单元格内容的基线对齐

行的基线由其所有单元格中初始单元格基线(即第一行文本的基线)中最低的定义。因此,在图 13-13 中,行的基线由第三个单元格定义,该单元格具有最低的初始基线。然后,前两个单元格的第一行文本的基线与行的基线对齐。

与顶部、中部和底部对齐一样,基线对齐单元格内容的放置是通过修改单元格的顶部和底部填充来完成的。如果一行中没有任何单元格是基线对齐的,则该行甚至没有基线 —— 它实际上不需要基线。

在行内对齐单元格内容的详细过程如下:

  • 如果任何单元格是基线对齐的,则确定行的基线并放置基线对齐的单元格的内容。

    • 任何顶部对齐的单元格都会放置其内容。现在,行有一个临时高度,由已经放置其内容的单元格中底部最低的单元格定义。

    • 如果有任何剩余的中部或底部对齐的单元格,并且内容高度高于临时行高度,则行的高度将增加以包含其中最高的单元格之一。

    • 所有剩余的单元格都放置其内容。在任何内容短于行高度的单元格中,将增加单元格的填充以匹配行的高度。

vertical-alignsubsupertext-toptext-bottom 在应用于表单元格时应被忽略。相反,它们似乎被视为 baseline 或可能是 top

摘要

即使你对表格布局非常熟悉,可能已经从多年的表格和空格设计中积累了经验,但事实证明,驱动这种布局的机制相当复杂。感谢 HTML 表格构建的遗产,CSS 表格模型以行为中心,但幸运的是,它确实支持列和有限的列样式。由于新的能力可以影响单元格对齐和表格宽度,现在你有更多的工具来以令人愉悦的方式呈现表格。

将表格相关的显示值应用于任意元素的能力,打开了通过使用 HTML 元素如<div><section>或使用允许您使用任何元素描述表格组件的 XML 语言来创建类似表格的布局的大门。

第十四章:字体

CSS1 规范中的“字体属性”部分,编写于 1996 年,以这句话开头:“设置字体属性将是样式表的最常见用途之一。” 尽管从 CSS 的最初开始就意识到字体的重要性,但直到大约 2009 年,这种能力才真正开始被广泛和一致地支持。随着可变字体的引入,网络上的排版已成为一种艺术形式。虽然您可以在设计中包含任何您有合法权限分发的字体,但您必须注意如何使用它们。

记住这一点并不意味着对字体有绝对控制权。如果您正在使用的字体无法下载,或者是用户浏览器不理解的文件格式,文本将(最终)显示为备用字体。这是件好事,因为这意味着用户仍然能够获取您的内容。

尽管字体对设计至关重要,但请始终记住,您不能依赖特定字体的存在。如果字体加载速度慢,浏览器通常会延迟文本呈现。虽然这可以防止用户阅读时重新绘制文本,但在页面上没有文本是不好的。

您的字体选择也可能被用户偏好或旨在增强阅读体验的浏览器扩展所覆盖。一个例子是浏览器扩展 OpenDyslexic,它“使用 OpenDyslexic 字体覆盖网页上的所有字体,并格式化页面以更容易阅读”。总体而言,始终设计时假定您的字体可能会延迟甚至完全失败。

字体族

我们所说的“字体”通常由许多变体组成,用于描述粗体文本、斜体文本、粗斜体文本等。例如,您可能熟悉(或至少听说过)Times 字体。Times 实际上是许多变体的组合,包括 TimesRegular、TimesBold、TimesItalic、TimesBoldItalic 等。Times 的每个变体都是一个实际的 字体面,而我们通常所认为的 Times 则是所有这些变体面的组合。换句话说,像 Times 这样的系统标准字体实际上是一个 字体族,而不仅仅是单个字体,尽管大多数人认为字体是单一实体。

对于这种字体族,每种宽度、重量和样式组合(即每种字体面)都需要单独的文件,这意味着对于完整的字体,您可能需要多达 20 个单独的文件。另一方面,可变字体 能够在单个文件中存储多个变体,例如常规、粗体、斜体和粗斜体。可变字体文件通常比单个字体面文件稍大一些(也许只有几千字节),但比普通字体所需的多个文件要小,并且只需要一个 HTTP 请求。

为了涵盖所有情况,CSS 定义了五个通用字体族:

衬线字体

衬线字体是比例的,并且有衬线。 如果字体中的所有字符具有不同的宽度,则称为比例字体。 例如,小写字母i和小写字母m占据不同的水平空间,因为它们具有不同的宽度。(例如,本书的段落字体是比例字体。) 衬线是每个字符内部笔画末端的装饰,例如小写字母l的顶部和底部的小线,或者大写字母A的每个腿部的底部。 衬线字体的示例包括 Times、Georgia 和 New Century Schoolbook 等。

无衬线字体

无衬线字体是比例的,不具有衬线。 无衬线字体的示例包括 Helvetica、Geneva、Verdana、Arial 和 Univers 等。

等宽字体

等宽字体是不比例的。 而是每个字符使用与其他所有字符相同的水平空间;因此,小写字母i占据与小写字母m相同的水平空间,即使它们的实际字形可能具有不同的宽度。 通常用于显示程序代码或表格数据,例如本书的代码字体。 如果字体具有统一的字符宽度,则被分类为等宽字体,无论其是否具有衬线。 等宽字体的示例包括 Courier、Courier New、Consolas 和 Andale Mono 等。

草书字体

草书字体试图模拟人类的手写或书法。 通常,它们主要由流动的曲线组成,并具有超过衬线字体的笔画装饰。 例如,大写字母A的左腿底部可能有一个小卷曲,或者完全由花折和卷曲组成。 草书字体的示例包括 Zapf Chancery、Author 和 Comic Sans 等。

奇幻字体

奇幻字体并没有任何单一特征来定义,除了我们无法轻易地将它们分类为其他字体族之一(有时这些字体被称为装饰展示字体)。 如西部、木刻和克林贡等几种字体。

您的操作系统和浏览器将为每个通用字体族拥有默认字体系列。 浏览器无法分类为有衬线、无衬线、等宽或草书的字体通常被视为奇幻字体。 虽然大多数字体族属于这些通用字体族之一,但并非全部。 例如,SVG 图标字体、dingbat 字体和 Material Icons Round 包含的是图像而不是字母。

使用通用字体系列

您可以使用属性font-family调用任何可用字体系列。

如果您希望文档使用无衬线字体,但并不特别关心使用哪一个,适当的声明如下:

body {font-family: sans-serif;}

这将导致用户代理选择无衬线字体族(如 Helvetica)并将其应用于<body>元素。多亏了继承,相同的字体族选择将被应用于所有从<body>派生的可见元素,除非被用户代理覆盖。用户代理通常对某些元素应用font-family属性,例如对于<code><pre>元素应用monospace,或对某些表单输入控件应用系统字体。

仅使用这些通用字体族,您就可以创建一个相当复杂的样式表。下面的规则集在图 14-1 中有所说明:

body {font-family: serif;}
h1, h2, h3, h4 {font-family: sans-serif;}
code, pre, kbd {font-family: monospace;}
p.signature {font-family: cursive;}

因此,本文档中的大部分内容将使用衬线字体,如 Times,包括所有段落,但具有classsignature的段落将使用草书字体,如 Author。标题级别 1 至 4 将使用无衬线字体,如 Helvetica,而元素<code><pre><tt><kbd>将使用等宽字体,如 Courier。

注意

使用通用默认值对于渲染速度非常有帮助,因为它允许浏览器使用已经在内存中的默认字体,而不是必须解析特定字体列表并按需加载字符。

css5 1401

图 14-1. 各种字体族

页面作者可能会对文档或元素的显示使用特定的字体有更具体的偏好。同样地,用户可能希望创建一个用户样式表,定义所有文档显示时要使用的确切字体。在任一情况下,font-family仍然是要使用的属性。

暂时假设所有<h1>元素应使用 Georgia 作为其字体。这样做的最简单规则如下:

h1 {font-family: Georgia;}

这将导致显示文档的用户代理对所有<h1>元素使用 Georgia 字体,假设用户代理已经有了可用的 Georgia 字体。如果没有,用户代理将无法使用该规则。它不会忽略该规则,但如果找不到名为Georgia的字体,它除了使用用户代理的默认字体来显示<h1>元素外,别无选择。

要处理这种情况,您可以通过结合特定字体族和通用字体族给用户代理提供选择。例如,以下标记告诉用户代理如果 Georgia 可用,则使用它,如果不可用,则使用另一个衬线字体,如 Times:

h1 {font-family: Georgia, serif;}

因此,我们强烈建议您始终在任何font-family规则中提供一个通用字体族。通过这样做,您提供了一个后备机制,让用户代理在无法提供精确字体匹配时选择替代方案。这通常被称为字体堆栈。以下是一些更多的示例:

h1 {font-family: Arial, sans-serif;}
h2 {font-family: Arvo, sans-serif;}
p {font-family: 'Times New Roman', serif;}
address {font-family: Chicago, sans-serif;}
.signature {font-family: Author, cursive;}

如果您熟悉字体,您可能会想到一些类似的字体来显示给定的元素。假设您希望文档中的所有段落都使用 Times 字体显示,但也接受 Times New Roman、Georgia、New Century Schoolbook 和 New York(这些都是衬线字体)作为备选选择。首先,确定这些字体的优先顺序,然后用逗号将它们串联起来:

p {font-family: Times, 'Times New Roman', 'New Century Schoolbook', Georgia,
      'New York', serif;}

根据此列表,用户代理将按照它们列出的顺序查找字体。如果找不到任何列出的字体,则会选择一个可用的衬线字体。

使用引号

您可能已经注意到在前面的代码示例中出现了单引号,这是本章中以前没有使用过的。只有在字体名称包含一个或多个空格(例如 New York)或字体名称包含符号时,建议使用引号。因此,名为 Karrank% 的字体应该加引号:

h2 {font-family: Wedgie, 'Karrank%', Klingon, fantasy;}

虽然几乎从不需要引用字体名称,但如果省略引号,用户代理可能会忽略字体名称,并继续使用字体堆栈中的下一个可用字体。这种情况的例外是与接受的 font-family 关键字匹配的字体名称。例如,如果您的字体名称是 cursiveserifsans-serifmonospacefantasy,必须加引号,以便用户代理区分字体名称和字体系列关键字,如下所示:

h2 {font-family: Author, "cursive", cursive;}

实际的通用系列名称(serifmonospace 等)绝不应加引号。如果加引号,浏览器将查找具有确切名称的字体。

在引用字体名称时,单引号或双引号都可以接受,只要匹配即可。请记住,如果您在 style 属性中放置 font-family 规则(通常不建议这样做),则需要使用未在属性本身中使用的引号。因此,如果您在双引号中包含 font-family 规则,您将需要在规则内部使用单引号,如下面的标记中所示:

p {font-family: sans-serif;}  /* sets paragraphs to sans-serif by default */
<!-- the next example is correct (uses single-quotes) -->
<p style="font-family: 'New Century Schoolbook', Times, serif;">...</p>

<!-- the next example is NOT correct (uses double-quotes) -->
<p style="font-family: "New Century Schoolbook", Times, serif;">...</p>

如果在这种情况下使用双引号,它们会干扰属性语法。请注意,字体名称不区分大小写。

使用自定义字体

@font-face 规则允许您在网页上使用自定义字体,而不仅仅依赖于“网页安全”字体(即广泛安装的字体系列,如 Times)。@font-face 规则的两个必需功能是声明用于引用字体的名称,并提供该字体文件的下载 URL。除了这些必需的描述符外,CSS 还有 14 个可选描述符。

虽然不能保证每个用户都能看到您想要的字体,但除了像 Opera Mini 这样出于性能原因故意不支持的浏览器外,几乎所有浏览器都支持 @font-face

假设您想在样式表中使用一个非常特定的字体,而这个字体并没有广泛安装。通过@font-face的魔法,您可以定义一个特定的家族名称,对应于服务器上的字体文件,您可以在整个 CSS 中引用它。用户代理将下载该文件并用它来呈现页面中的文本,就像它被安装在用户的机器上一样。例如:

@font-face {
    font-family: "Switzera";
    src: url("SwitzeraADF-Regular.otf");
}

这允许您告知用户代理加载定义的.otf文件,并在调用时使用该字体来呈现文本,使用font-family: SwitzeraADF

注意

本节中的示例涉及 SwitzeraADF,这是从Arkandis Digital Foundry提供的字体集。

@font-face声明不会自动加载所有引用的字体文件。@font-face的目的是允许惰性加载字体。这意味着只有渲染文档所需的字形才会被加载。在您的 CSS 中引用但不需要渲染页面的字体文件将不会被下载。字体文件通常会被缓存,并且在用户导航您的站点时不会重新下载。

能够加载任何字体是非常强大的,但请牢记这些问题:

  • 出于安全原因,字体文件必须从请求它们的页面相同的域中检索。有一个解决方案。

  • 需要大量字体下载可能导致加载时间缓慢。

  • 字符较多的字体可能导致较大的字体文件。幸运的是,在线工具和 CSS 使得可以限制字符集。

  • 如果字体加载缓慢,这可能导致未样式化文本的闪烁或不可见文本。CSS 也有办法解决这个问题。

我们将在本章中讨论这些问题及其解决方案。但请记住,伴随着巨大的权力而来的是巨大的责任。明智地使用字体!

使用字体描述符

您引用的定义字体的所有参数都包含在@font-face { }结构中。这些被称为描述符,非常类似于属性,它们采用*descriptor*: *value*;的格式。实际上,大多数描述符名称直接参考属性名称,这将在本章的其余部分中进行详细探讨。表 14-1 列出了可能的描述符,包括必需和可选的。

表 14-1. 字体描述符

描述符 默认值 描述
font-family n/a 必需.font-family属性值中使用的此字体的名称。
src n/a 必需. 指向必须加载以显示字体的字体文件的一个或多个 URL。
font-display auto 根据字体面何时下载和准备使用确定字体面的显示方式。
font-stretch normal 区分不同字符宽度的程度(例如condensedexpanded)。
font-style normal 区分normalitalicoblique字形。
font-weight normal 区分各种权重(例如,bold)。
font-variant normal font-variant 属性的一个值。
font-feature-settings normal 允许直接访问低级别的 OpenType 功能(例如,启用连字)。
font-variation-settings normal 允许通过指定要变化的特征的四字母轴名称及其变化值,对 OpenType 或 TrueType 字体变化进行低级别控制。
ascent-override normal 定义字体的上升度度量。
descent-override normal 定义字体的下降度度量。
line-gap-override normal 定义字体的行间隙度量。
size-adjust 100% 定义字体轮廓和与字体相关的度量的乘数。
unicode-range U+0-10FFFF 定义给定字体面可以使用的字符范围。

如 表 14-1 中所述,font-familysrc 需要两个描述符。

src 的意图非常直接,所以我们将首先描述它:src 允许您为您定义的字体面提供一个或多个逗号分隔的来源。对于每个来源,您可以提供一个可选的(但建议的)格式提示,这可以帮助提高下载性能。

您可以指向任何 URL 上的字体面,包括使用 local() 在用户计算机上的文件,以及使用 url() 在其他地方的文件。有一个默认限制:除非设置了例外,否则字体面只能从与样式表相同的源加载。您不能简单地将您的 src 指向别人的站点并下载他们的字体。您需要在自己的服务器上托管本地副本,使用 HTTP 访问控制来放宽相同域的限制,或使用提供样式表和字体文件的字体托管服务。

注意

要为字体创建同源限制的例外,请在您服务器的 .htaccess 文件中包含以下内容:

<FilesMatch "\.(ttf|otf|woff|woff2)$">
  <IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin "*"
  </IfModule>
</FilesMatch>

FilesMatch 行包括您想导入的字体文件的所有文件扩展名。这将允许任何人从任何地方指向您的字体文件并直接从您的服务器加载它们。

或许您会想知道,我们在这里如何定义 font-family,当它已经在之前的部分中定义过。这里的 font-family 是字体族 描述符,而之前定义的 font-family 是字体族 属性。如果这看起来令人困惑,请稍等片刻,一切将变得清晰。

实质上,@font-face 允许您创建底层定义,支撑像 font-family 这样的与字体相关的属性。当您通过描述符 font-family: "Switzera"; 定义一个字体族名称时,您正在为用户代理的字体族表中设置一个条目,您可以在您的 font-family 属性值中引用它:

@font-face {
    font-family: "Switzera";   /* descriptor */
    src: url("SwitzeraADF-Regular.otf");
}
h1 {font-family: switzera, Helvetica, sans-serif;}  /* property */

注意,font-family 描述符值和 font-family 属性中的条目是不区分大小写匹配的。如果它们完全不匹配,h1 规则将会忽略 font-family 值中列出的第一个字体名称并转向下一个(在本例中为 Helvetica)。

还要注意,font-family 描述符可以是(几乎)任何您想要的名称。它不必完全匹配字体文件的名称,尽管通常出于清晰性的目的,使用与字体名称至少接近的描述符是有意义的。也就是说,font-family 属性中使用的值必须(不区分大小写地)与 font-family 描述符匹配。

只要字体已经下载完毕并且是用户代理能够处理的格式,它将按照您指定的方式使用,正如在 图 14-2 所示。

css5 1402

图 14-2. 使用下载的字体

类似地,逗号分隔的 src 描述符值可以提供备用方案。这样,如果用户代理程序不理解由提示定义的文件类型,或者由于某种原因无法下载第一个源,它可以转而尝试加载其中定义的第二个源:

@font-face {
    font-family: "Switzera";
    src: url("SwitzeraADF-Regular.otf"),
         url("https://example.com/fonts/SwitzeraADF-Regular.otf");
}

请记住,前面提到的同源策略通常也适用于此情况,因此指向其他服务器上字体副本的尝试通常会失败,除非该服务器设置为允许跨源访问。

如果您希望确保用户代理程序了解您告诉它要使用的字体类型,请使用可选但强烈推荐的 format() 提示:

@font-face {
    font-family: "Switzera";
    src: url("SwitzeraADF-Regular.otf") format("opentype");
}

提供 format() 提示的优势在于,用户代理可以跳过不支持的文件格式的下载,从而减少带宽使用和加载时间。如果未提供格式提示,即使其格式不受支持,字体资源也将被下载。format() 提示还允许您明确声明可能没有常见文件扩展名的文件的格式:

@font-face {
    font-family: "Switzera";
    src: url("SwitzeraADF-Regular.otf") format("opentype"),
         url("SwitzeraADF-Regular.true") format("truetype");
         /* TrueType font files usually end in '.ttf' */
}

表 14-2 列出了截至 2022 年底允许的所有格式值。

表 14-2. 已识别的字体格式值

格式 完整名称
collection OTC/TTC 开放字体集合(前身为:TrueType 集合)
embedded-opentype EOT 嵌入式 OpenType
opentype OTF 开放字体
svg SVG 可缩放矢量图形
truetype TTF TrueType
woff2 WOFF2 Web 开放字体格式,版本 2
woff WOFF Web 开放字体格式

除了格式外,您还可以使用 tech() 函数提供与字体技术对应的值。Switzera 的彩色字体版本可能如下所示:

@font-face {
    font-family: "Switzera";
    src: url("SwitzeraADF-Regular-Color.otf")
            format("opentype") tech("color-COLRv1"),
         url("SwitzeraADF-Regular.true") format("truetype");
         /* TrueType font files usually end in '.ttf' */
}

表 14-3 列出了截至 2022 年底已识别的所有字体技术值。

表 14-3. 已识别的字体技术值

描述
color-CBDT 字体颜色使用 OpenType 的CBDT(颜色位图数据表)表格定义。
color-COLRv0 字体颜色使用 OpenType 的COLR(颜色表)表格定义。
color-COLRv1 字体颜色使用 OpenType 的COLR表格定义。
color-sbix 字体颜色使用 OpenType 的sbix(标准位图图形表)表格定义。
color-SVG 字体颜色使用 OpenType 的SVG(可伸缩矢量图形)表格定义。
feature-aat 字体使用 Apple 高级排版(AAT)字体特性注册表的表格。
feature-graphite 字体使用来自 Graphite 开源字体渲染引擎的表格。
feature-opentype 字体使用来自 OpenType 规范的表格。
incremental 使用范围请求或修补子集服务器方法进行逐步加载字体。
palettes 字体通过 OpenType 的CPAL表格提供调色板。
variations 字体使用 OpenType 表格定义的变体,如GSUBGPOS,AAT 表格morxkerx,或 Graphite 表格SilfGlatGlocFeatSill

深入研究所有这些特征表格的细节远远超出了本书的范围,大部分时间你不太可能需要使用它们。即使字体具有列出的一个或多个特征表,也不需要列出它们。即使使用tech("color-SVG"),SVG 颜色字体仍将使用其颜色进行渲染。

除了url()format()tech()的组合外,您还可以使用名为local()的适当命名的函数在用户计算机上使用已经存在的字体族名称(或几个名称)。:

@font-face {
    font-family: "Switzera";
    src: local("Switzera-Regular"),
         local("SwitzeraADF-Regular"),
         url("SwitzeraADF-Regular.otf") format("opentype"),
         url("SwitzeraADF-Regular.true") format("truetype");
}

在此示例中,用户代理会查看是否已经在本地计算机上安装了名为Switzera-RegularSwitzeraADF-Regular的字体族,不区分大小写。如果是,则将使用名称Switzera引用该本地安装字体。如果没有,则将尝试下载第一个远程字体,其格式类型浏览器支持。

请注意,src中列出的资源顺序非常重要。一旦浏览器遇到支持的格式,它将尝试使用该源。因此,local()值应首先列出,无需格式提示。接下来是外部资源,按文件大小从小到大的顺序列出,以最小化性能影响。

此功能允许作者为本地安装的字体创建自定义名称。例如,您可以为 Hiragino 的各个版本设置较短的名称,如下所示:

@font-face {
    font-family: "Hiragino";
    src: local("Hiragino Kaku Gothic Pro"),
         local("Hiragino Kaku Gothic Std");
}

h1, h2, h3 {font-family: Hiragino, sans-serif;}

只要用户在其计算机上安装了 Hiragino Kaku Gothic 的任一版本,这些规则将导致前三个标题级别使用该字体进行渲染。

在线服务可以让您上传字体文件并生成所有所需的 @font-face 规则,将这些文件转换为所有必需的格式,并将所有内容作为单个包返回给您。其中最著名的之一是 Font Squirrel's @Font-Face Kit Generator。只需确保您有法律上的能力转换和使用通过生成器运行的字体(有关更多信息,请参阅以下边栏)。

限制字符范围

有时,您可能希望在非常有限的情况下使用自定义字体;例如,确保字体仅应用于特定语言的字符。在这些情况下,限制字体仅用于某些字符或符号可能是有用的,而 unicode-range 描述符正好允许这样做。

默认情况下,此描述符的值覆盖了 U+0 到 U+10FFFF 的整个 Unicode 范围,这意味着如果字体可以提供字符的字形,它将会提供。大多数情况下,这正是你想要的。在其他情况下,你会希望针对特定类型的内容使用特定的字体。你可以定义单个码点、码点范围或带有 ? 通配符字符的一组范围。

从 CSS 字体模块 3 级挑选几个例子:

unicode-range: U+0026; /* the Ampersand (&) character */
unicode-range: U+590-5FF;  /* Hebrew characters */
unicode-range: U+4E00-9FFF, U+FF00-FF9F, U+30??, U+A5;  /* Japanese
 kanji, hiragana, and katakana, plus the yen/yuan currency symbol*/

在第一种情况下,指定了单个码点。字体仅在和符号(&)字符中使用。如果未使用 and 字符,则不下载字体。如果使用,则下载整个字体文件。因此,有时最好优化字体文件,仅包括指定 Unicode 范围内的字符,尤其是在这种情况下,您仅使用了包含数千个字符的字体中的一个字符。

在第二种情况下,指定了一个单一范围,跨越 Unicode 字符码点 590 到 5FF。这涵盖了在写希伯来语时使用的 111 个字符。因此,作者可以指定希伯来字体,并将其限制为仅用于希伯来字符,即使该字体包含其他码点的字形:

@font-face {
    font-family: "CMM-Ahuvah";
    src: url("cmm-ahuvah.otf" format("opentype");
    unicode-range: U+590-5FF;
}

在第三种情况下,通过以逗号分隔的列表指定一系列范围来覆盖所有日文字符。那里的有趣特性是 U+30?? 值,带有一个问号,这是在 unicode-range 值中允许的特殊格式。问号是通配符,意味着“任何可能的数字”,使得 U+30?? 等同于 U+3000-30FF。问号是值中唯一允许的“特殊”字符模式。

范围必须始终是升序。任何降序范围,例如 U+400-300,都将被视为解析错误并被忽略。

因为@font-face被设计为优化延迟加载,可以使用unicode-range仅下载页面实际需要的字体面,当使用仅包含定义的子集字符范围的字体文件时,文件大小可能大大减小。如果页面不使用范围内的任何字符,则不会下载字体。如果页面上的单个字符需要字体,则整个字体将被下载。

假设您拥有一个网站,该网站混用英语、俄语和基本数学运算符,但您不知道每个页面上会出现哪些内容。页面可能全是英语,混合了俄语和数学符号等等。此外,假设您有三种类型内容的特殊字体。您可以通过正确构建的一系列@font-face规则,确保用户代理仅下载其所需的字体面。

@font-face {
    font-family: "MyFont";
    src: url("myfont-general.otf" format("opentype");
}
@font-face {
    font-family: "MyFont";
    src: url("myfont-cyrillic.otf" format("opentype");
    unicode-range: U+04??, U+0500-052F, U+2DE0-2DFF, U+A640-A69F, U+1D2B-1D78;
}
@font-face {
    font-family: "MyFont";
    src: url("myfont-math.otf" format("opentype");
    unicode-range: U+22??;   /* equivalent to U+2200-22FF */
}

body {font-family: MyFont, serif;}

因为第一条规则没有指定 Unicode 范围,所以始终会下载整个字体文件——除非页面恰好不包含任何字符(甚至可能)。第二条规则只有在页面包含其声明的 Unicode 范围内的字符时,才会下载myfont-cyrillic.otf;第三条规则对数学符号同样如此。

如果内容需要数学字符 U+2222(∢,球面角字符),将下载myfont-math.otf,并使用来自myfont-math.otf的字符,即使myfont-general.otf中已有该字符。

更可能使用此功能的方式是我们的&示例;我们可以从草写体字体中包含一个漂亮的&并用它代替标题字体中的&。类似这样:

@font-face {
    font-family: "Headline";
    src: url("headliner.otf" format("opentype");
}
@font-face {
    font-family: "Headline";
    src: url("cursive-font.otf" format("opentype");
    unicode-range: U+0026;
}

h1, h2, h3, h4, h5, h6 {font-face: Headline, cursive;}

在这种情况下,为了保持页面重量轻,选择一个草写体(您有使用权的)并将其最小化以仅包含&字符。您可以使用类似 Font Squirrel 的工具创建单字符字体文件。

注意

请记住,页面可以使用 Google Translate 等自动化服务进行翻译。如果您过于严格地限制 Unicode 范围(例如,仅限于英语中使用的无重音字母范围),那么页面被自动翻译成法语或瑞典语时,可能会混杂使用不同字体面的字符,因为这些语言中的重音字符将使用回退字体,而无重音字符将使用您预期的字体。

处理字体显示

如果您是某个特定时期的设计师或开发人员,您可能还记得未样式化内容的闪烁(FOUC)。这种情况发生在早期浏览器加载 HTML 并在 CSS 加载完成之前(或至少在通过 CSS 完成页面布局之前)将其显示到屏幕上。

未样式化文本的闪烁FOUT)发生在浏览器加载页面、CSS 并显示布局页面及所有文本之前,此时尚未加载自定义字体。FOUT 导致文本以默认字体或回退字体显示,然后被使用自定义加载字体的文本替换。

这个问题的姊妹问题是不可见文本的闪烁FOIT)。这种用户代理解决方案是由于浏览器检测到文本设置为尚未加载的自定义字体时,使文本变得不可见,直到字体加载或特定时间过去为止。

由于文本替换可能会改变其大小,无论是通过 FOUT 还是 FOIT,请在选择回退字体时谨慎。如果初始显示文本的字体与最终加载和使用的自定义字体之间存在显著的高度差异,可能会发生显著的页面重新布局。

为了解决这个问题,font-display 描述符指导浏览器在网页字体尚未加载时进行文本渲染。

我们可以称之为字体显示时间轴计时器,它从用户代理首次绘制页面开始。时间轴分为三个阶段:阻塞、交换和失败。

字体阻塞期期间,如果字体加载失败,浏览器将使用一个不可见的回退字体来渲染应该使用该字体的任何内容,意味着文本内容不可见但保留空间。如果在阻塞期间成功加载字体,则文本将使用下载的字体进行渲染并变得可见。

交换期期间,如果字体加载失败,浏览器将使用一个可见的回退字体来渲染内容,很可能是本地安装的字体(例如 Helvetica)。如果字体成功加载,回退字体将被下载的字体替换。

一旦进入失败期,用户代理将视请求的字体加载为失败,回退到可用字体,并且如果最终加载了字体,不会再交换字体。如果交换期是无限的,就永远不会进入失败期。

font-display 描述符的值与这些时间轴的时期相匹配,它们的效果是强调时间轴的某一部分,而牺牲其他部分。其效果总结在 Table 14-4 中。

表格 14-4. font-display 的值

阻塞期间^(a) 交换期间^(a) 失败期间^(a)
auto 浏览器定义 浏览器定义 浏览器定义
block 3s 无限 n/a
swap < 100 ms 无限 n/a
fallback < 100 ms 3s 无限
optional 0 0 无限
^(a) 推荐的期间长度;实际时间可能有所不同

让我们依次考虑每个值:

block

告诉浏览器为字体保留几秒钟的空间(规范建议为 3 秒,但浏览器可能选择自己的值),然后进入无限长的交换期。如果字体最终加载,即使是 10 分钟后,将用其替换在其位置使用的备用字体。

swap

类似,只是不会保持空间开放超过几分之一秒(建议 100 毫秒)。然后使用备用字体,并在最终加载时替换为预期的字体。

fallback

给出与swap相同的简短块窗口,然后进入一个短暂的期间,在此期间备用字体可以被预期的字体替换。如果超过这个短暂的期间(建议 3 秒),将永久使用备用字体,并且用户代理可能会取消预期字体的下载,因为永远不会发生交换。

optional

其中最严格的是:如果字体在首次绘制时不可用,用户代理将直接转向备用字体,并跳过块和交换期,进入页面生命周期的失败期。

结合描述符

可能不太明显的是,您可以提供多个描述符,以便为特定的属性组合分配特定的面孔。例如,您可以为粗体文本分配一个面孔,为斜体文本分配另一个面孔,为既粗体又斜体的文本分配第三个面孔。

这种能力是隐含的,因为任何未声明的描述符都被分配其默认值。让我们考虑一组基本的三个面孔分配,使用我们已经涵盖的描述符和稍后会介绍的一些描述符:

@font-face {
    font-family: "Switzera";
    font-weight: normal;
    font-style: normal;
    font-stretch: normal;
    src: url("SwitzeraADF-Regular.otf") format("opentype");
}
@font-face {
    font-family: "Switzera";
    font-weight: 500;
    font-style: normal;
    font-stretch: normal;
    src: url("SwitzeraADF-Bold.otf") format("opentype");
}
@font-face {
    font-family: "Switzera";
    font-weight: normal;
    font-style: italic;
    font-stretch: normal;
    src: url("SwitzeraADF-Italic.otf") format("opentype");
}

您可能已经注意到,我们已经显式声明了一些描述符及其默认值,尽管我们并不需要。前面的示例与一组三条规则完全相同,我们删除了显示值为normal的每个描述符:

@font-face {
   font-family: "Switzera";
   src: url("SwitzeraADF-Regular.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: 500;
   src: url("SwitzeraADF-Bold.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-style: italic;
   src: url("SwitzeraADF-Italic.otf") format("opentype");
}

在所有三条规则中,超出默认normal量的字体拉伸不会发生,并且font-weightfont-style的值因分配的面孔而异。那么,如果我们想为未拉伸的既加粗又斜体的文本分配特定的面孔呢?

@font-face {
   font-family: "Switzera";
   font-weight: bold;
   font-style: italic;
   font-stretch: normal;
   src: url("SwitzeraADF-BoldItalic.otf") format("opentype");
}

那么加粗、斜体、压缩文本呢?

@font-face {
   font-family: "Switzera";
   font-weight: bold;
   font-style: italic;
   font-stretch: condensed;
   src: url("SwitzeraADF-BoldCondItalic.otf") format("opentype");
}

正常重量、斜体、压缩文本呢?

@font-face {
   font-family: "Switzera";
   font-weight: normal;
   font-style: italic;
   font-stretch: condensed;
   src: url("SwitzeraADF-CondItalic.otf") format("opentype");
}

我们可以继续这样做很长一段时间,但是让我们就此打住。如果我们采取所有这些规则,并剥离任何具有normal值的内容,我们最终得到以下结果,如图 14-3 所示:

@font-face {
   font-family: "Switzera";
   src: url("SwitzeraADF-Regular.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: bold;
   src: url("SwitzeraADF-Bold.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-style: italic;
   src: url("SwitzeraADF-Italic.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: bold;
   font-style: italic;
   src: url("SwitzeraADF-BoldItalic.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: bold;
   font-stretch: condensed;
   src: url("SwitzeraADF-BoldCond.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-style: italic;
   font-stretch: condensed;
   src: url("SwitzeraADF-CondItalic.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: bold;
   font-style: italic;
   font-stretch: condensed;
   src: url("SwitzeraADF-BoldCondItalic.otf") format("opentype");
}

css5 1403

图 14-3. 使用各种面孔

如果你声明 html { +font-family: switzera;},那么在使用 switzera 的其他选择器中不需要再次声明字体族。浏览器会根据你为 font-weightfont-stylefont-stretch 属性值设置的选择器特定值,为粗体、斜体、拉伸和普通文本选择正确的字体文件。

关键是,我们可以为每种粗细、样式和拉伸设置特定的字体文件。通过几个 @font-face 规则使用同一个 font-family 名称声明所有变体,确保了统一的字体设计,即使在使用非可变字体时也避免了字体合成。通过 @font-face 声明字体的所有变体,使用相同的 font-family 描述符名称,减少了 font-family 属性的覆盖,降低了团队中其他开发人员为特定选择器错误使用字体文件的可能性。

正如你所见,使用标准字体时,这三个描述符可能有大量的组合方式——考虑到 font-stretch 有 10 种可能的值——但你可能永远不必尝试全部。事实上,大多数字体族没有像 SwitzeraADF 提供的那么多字体(截至最后统计为 24 种),因此将所有可能性写出来可能没有多少意义。尽管如此,这些选项确实存在,在某些情况下,你可能会发现需要为粗体压缩文本指定特定的字体,以便用户代理不会尝试为你计算它们。或者使用具有权重和收缩轴的可变字体。

现在我们已经介绍了 @font-face 并概述了几个描述符,让我们回到属性。

字体粗细

大多数人习惯于普通和粗体文本,这是两种最基本的字体粗细。CSS 通过 font-weight 属性让你对字体粗细有更多控制。

<number> 的值可以从 11000,包括这两个数值,其中 1 为最轻,1000 为最重的可能粗细。除非使用稍后讨论的可变字体,否则字体族通常提供有限的粗细选择(有时只有一个单一的粗细)。

一般来说,字体的粗细增加,字体看起来越加深和“更加粗”。有许多方法可以标记重体字体。例如,被称为 SwitzeraADF 的字体族有诸如 SwitzeraADF Bold、SwitzeraADF Extra Bold、SwitzeraADF Light 和 SwitzeraADF Regular 等变体。所有这些字体使用相同的基本字体形状,但每种都有不同的粗细。

如果指定的字重不存在,将使用最接近的字重。表格 14-5 列出了用于每个通常接受的字重标签的数字,如在 "wght" 变化轴中定义的。如果字体仅有两种字重对应 400700(普通和粗体),则任何font-weight值将映射到最接近的值。因此,任何 1550font-weight 值将映射到 400,而任何大于 5501000 的值将映射到 700

表格 14-5. 字重映射

Value 映射
1 最低有效值
100 细体
200 超轻(超极轻)
300 轻体
400 普通
500 中等
600 半粗(中等粗体)
700 粗体
800 超粗(超极粗)
900 黑体(重体)
950 超黑(超极黑)
1000 最高有效值

假设您希望在文档中使用 SwitzeraADF,但希望利用所有这些字重级别。如果用户在其计算机上具有所有字体文件,并且您没有使用 @font-face 来重命名所有选项为 Switzera,则可以直接通过 font-family 属性引用它们,但实际上你不应该这样做。编写这样的样式表不是一件有趣的事情:

h1 {font-family: 'SwitzeraADF Extra Bold', sans-serif;}
h2 {font-family: 'SwitzeraADF Bold', sans-serif;}
h3 {font-family: 'SwitzeraADF Bold', sans-serif;}
h4, p {font-family: 'SwitzeraADF Regular', sans-serif;}
small {font-family: 'SwitzeraADF Light', sans-serif;}

这相当乏味。这正是为什么通过为整个文档指定单一字体系列,然后通过 @font-face 为各个元素分配不同字重如此强大的典范:可以包含几个 @font-face 声明,每个都具有相同的 font-family 名称,但具有不同的 font-weight 描述符值。然后可以使用不同的字体文件进行相对简单的 font-weight 声明:

strong {font-weight: bold;}
b {font-weight: bolder;}

第一个声明表示应使用粗体字体或者换句话说,比普通字体更重的字体来显示<strong>元素。第二个声明表示<b>应使用继承的font-weight值再加上 100 的字体。

实际发生的事情是使用字体的更重版本来显示 <strong><b> 元素。因此,如果您使用 Times 显示段落,并且其中的一部分是粗体,则实际上使用了同一字体的两个版本:Times 和 TimesBold。正常文本使用 Times 显示,而粗体文本则使用 TimesBold 显示。

如果字体没有粗体版本,则浏览器可能会合成伪粗体。(要防止此情况,请使用稍后描述的 font-synthesis 属性。)

字重的工作原理

要理解用户代理如何确定给定字体变体的重量(以及如何继承重量),最简单的方法是从谈论11000之间的值开始,特别是那些可被 100 整除的值,即100900。这些数字值被定义为映射到字体设计中一个相对常见的特性,即给字体九个权重级别。如果非变量字体族具有所有九个权重级别的面孔,则数字直接映射到预定义级别,其中100作为字体的最轻变体,900作为最重变体。

实际上,这些数字本身没有固有的权重。CSS 规范只是说每个数字对应至少与前一个数字相同重的变体。因此,100200300400可能映射到一个相对较轻的变体;500600可能对应一个中等重的变体;700800900可能都产生相同非常重的字体变体。只要没有数字对应于比前一个较低数字分配的变体更轻,一切都会没问题。

对于非变量字体,这些数字被定义为等同于某些常见的变体名称。值400被定义为等同于normal,而700对应于bold

如果一个字体族少于九个权重,则用户代理必须以预定的方式填补这些空白:

  • 如果值500未分配,则赋予与400相同的字重。

  • 如果300未分配,则赋予比400更轻的下一个变体。如果没有更轻的变体可用,则300被分配与400相同的变体。在这种情况下,通常是 Normal 或 Medium。这种方法也适用于200100

  • 如果600未分配,则赋予比分配给500的变体更暗的下一个变体。如果没有更暗的变体可用,则600被分配与500相同的变体。这种方法也适用于700800900

为了更清楚地说明这种权重方案,让我们看几个例子。在第一个例子中,假设字体族 Karrank%是一个 OpenType 字体,因此已经定义了九种权重。在这种情况下,数字被分配给每个级别,关键字normalbold分别分配给数字400700

在我们的第二个例子中,考虑字体族 SwitzeraADF。假设其变体可能被分配数字值作为font-weight,如表 14-6 所示。

表 14-6. 特定字体族的假设权重分配

字体 指定关键字 指定数字
SwitzeraADF Light 100300
SwitzeraADF Regular normal 400
SwitzeraADF Medium 500
SwitzeraADF Bold bold 600700
SwitzeraADF Extra Bold 800900

前三个数字值分配给最轻的权重。常规字体获得关键字normal和数字权重400。由于有一个中等字体,它被分配给数字500。没有任何内容分配给600,因此它被映射到粗体字体,这也是分配给700bold的变体。最后,800900分配给黑体和超黑体变体。请注意,只有在这些字体已经分配了前两个最高权重级别时,才会发生最后的分配。否则,用户代理可能会忽略它们,并将800900分配给粗体字体,或者将它们都分配给黑体变体中的一个。

font-weight属性是继承的,因此如果您将段落设置为bold

p.one {font-weight: bold;}

那么它的所有子元素都将继承该粗体效果,正如我们在图 14-4 中看到的。

css5 1404

图 14-4. 继承的字体粗细

这并不罕见,但当您使用我们要讨论的最后两个值:bolderlighter时,情况变得有趣。一般来说,这些关键字的效果是您预期的:它们使文本相对于其父元素的字体重量更加粗或更加细。它们如何做到这一点略微复杂。首先,让我们考虑bolder

如果您将元素设置为bolderlighter的权重,用户代理首先必须确定从父元素继承的font-weight值是多少。一旦它有了这个数字(比如,400),然后根据表 14-7 中所示的值进行更改。

表 14-7. bolderlighter权重映射

继承的值 bolder lighter
value < 100 400 无变化
100 ≤ value < 350 400 100
350 ≤ value < 550 700 100
550 ≤ value < 750 900 400
750 ≤ value < 900 900 700
900 ≤ value 无变化 700

因此,您可能会遇到以下情况,如图 14-5 所示:

p {font-weight: normal;}
p em {font-weight: bolder;}  /* inherited value '400', evaluates to '700' */

h1 {font-weight: bold;}
h1 b {font-weight: bolder;}  /* inherited value '700', evaluates to '900' */

div {font-weight: 100;}
div strong {font-weight: bolder;} /* inherited value '100', evaluates to '400' */

css5 1405

图 14-5. 尝试加粗的文本

在第一个示例中,用户代理从400上升到700。在第二个示例中,<h1>文本已经设置为bold,相当于700。如果没有更粗的字体可用,用户代理将<h1>中的<b>文本的权重设置为900,因为这是从700到下一个级别的步骤。由于900分配给与700相同的字体,因此正常的<h1>文本和粗体的<h1>文本之间没有可见的区别,但权重值仍然不同。

正如您所预期的那样,lighter的工作方式基本相同,只是导致用户代理向下移动权重比例,而不是向上。

字体粗细描述符

使用 font-weight 描述符,作者可以为 font-weight 属性允许的加权级别分配不同权重的面。该描述符支持 autonormalbold,或一到两个数值作为范围。不支持 lighterbolder

例如,以下规则明确将五个字体面赋给六个 font-weight 值:

@font-face {
   font-family: "Switzera";
   font-weight: 1 250;
   src: url("f/SwitzeraADF-Light.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: normal;
   src: url("f/SwitzeraADF-Regular.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: 500 600;
   src: url("f/SwitzeraADF-DemiBold.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: bold;
   src: url("f/SwitzeraADF-Bold.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-weight: 800 1000;
   src: url("f/SwitzeraADF-ExtraBold.otf") format("opentype");
}

拥有这些指定的字体,作者现在可以利用多个加权级别,如图 14-6 所示:

h1, h2, h3, h4 {font-family: SwitzeraADF, Helvetica, sans-serif;}
h1 {font-size: 225%; font-weight: 900;}
h2 {font-size: 180%; font-weight: 700;}
h3 {font-size: 150%; font-weight: 500;}
h4 {font-size: 125%; font-weight: 300;}

css5 1406

图 14-6. 使用声明的 font-weight 字体面

在任何情况下,用户代理根据 font-weight 属性的确切值选择要使用的字体面,使用详细的分辨率算法,详见“权重如何工作”。虽然 font-weight 属性有许多关键字值,但 font-weight 描述符只接受 normalbold 作为关键字,以及从 11000(包括)的任何数字。

字体大小

尽管大小没有 @font-face 描述符,但您需要理解 font-size 属性,以更好地理解即将介绍的一些描述符,因此我们现在来探讨它。确定字体大小的方法既非常熟悉又非常不同。

最初可能令人费解的是,被声明为相同大小的不同字体可能看起来不一样大。这是因为 font-size 属性与实际渲染结果之间的关系由字体设计者决定。这种关系设置为字体本身的 em 方框(有些人称之为 em 方框),在没有额外行距(CSS 中的 `line-height`)的情况下,它指的是基线之间的距离。

font-size 的效果是为给定字体的 em 方框提供一个大小。这并不保证显示的字符会是这个大小。事实上,字体可能有比基线之间默认距离更高的字符。而且,许多字体定义可能所有字符都比其 em 方框小。图 14-7 展示了一些假设的例子。

css5 1407

图 14-7. 字体字符和 em 方框

使用绝对大小

在这一切都已经明确的基础上,现在我们转向绝对大小关键字。font-size 属性有八个绝对大小值:xx-smallx-smallsmallmediumlargex-largexx-large,以及相对较新的 xxx-large。这些值没有精确定义,而是相对于彼此定义,如图 14-8 所示:

p.one {font-size: xx-small;}
p.two {font-size: x-small;}
p.three {font-size: small;}
p.four {font-size: medium;}
p.five {font-size: large;}
p.six {font-size: x-large;}
p.seven {font-size: xx-large;}
p.eight {font-size: xxx-large;}

css5 1408

图 14-8. 绝对字体大小

在 CSS1 规范中,一个绝对大小与下一个之间的差异(或缩放因子)为 1.5,这被认为是一个过大的缩放因子。在 CSS2 中,计算机屏幕上相邻索引之间的建议缩放因子为 1.2。然而,这并没有解决所有问题,因为它对小尺寸造成了问题。

CSS Fonts Level 4 规范并没有一个适用于所有的缩放因子。相反,每个绝对大小关键字值都有一个基于medium值的特定大小缩放因子(参见 Table 14-8)。small的值被列为medium的八分之九大小,而xx-small是三分之五。无论如何,这些缩放因子都是指导方针,用户代理可以出于任何原因自由地修改它们。

表 14-8. 字体大小映射

CSS 绝对大小值 xx-small x-small small medium large x-large xx-large xxx-large
缩放因子 3/5 3/4 8/9 1 6/5 3/2 2/1 3/1
medium==16px的大小 9px 10px 13px 16px 18px 24px 32px 48px
HTML 标题等效 h6 - h5 h4 h3 h2 h1 n/a

请注意,我们明确将默认大小medium设置为16px。所有通用字体族的默认font-size值都是相同的medium,但基于操作系统或浏览器用户设置,medium关键字可能有不同的定义。例如,在许多浏览器中,serif 和 sans-serif 字体的medium等于16px,但monospace设置为13px

警告

截至 2022 年底,xxx-large关键字在 Safari 或 Opera 上,无论是桌面版还是移动版,都不支持。

使用相对大小

就像font-weight有关键词bolderlighter一样,font-size属性有相对大小关键词largersmaller。与相对字重一样,这些关键词导致font-size的计算值在一个大小值的比例尺上上下移动。

largersmaller关键字相对较为直接:它们导致元素的大小相对于其父元素在绝对大小比例上上移或下移:

p {font-size: medium;}
strong, em {font-size: larger;}
<p>This paragraph element contains <strong>a strong-emphasis element,
which itself contains <em>an emphasis element, which also contains
<strong>a strong element.</strong></em></strong></p>

<p> medium <strong>large <em> x-large <strong>xx-large</strong> </em> </strong>
    </p>

与字重的相对值不同,相对大小值不一定受到绝对大小范围的限制。因此,字体的大小可以超出xx-smallxxx-large的大小。如果父元素的font-size是最大或最小的绝对值,浏览器将使用 1.2 到 1.5 之间的缩放因子来创建更小或更大的字体大小。例如:

h1 {font-size: xxx-large;}
em {font-size: larger;}
<h1>A Heading with <em>Emphasis</em> added</h1>
<p>This paragraph has some <em>emphasis</em> as well.</p>

正如你在图 14-9 中所看到的,<h1> 元素中的强调文本略大于 xxx-large。缩放的量由用户代理决定,首选缩放因子在 1.2 到 1.5 的范围内,但不是必须的。段落中的 em 文本向上移动一个插槽至 140%。

css5 1409

图 14-9. 绝对大小边缘的相对字体大小
警告

用户代理不要求在绝对大小关键字的限制之外增加或减少字体大小,但它们仍可能这样做。此外,尽管在技术上可以声明比 xx-small 更小,但小文字在屏幕上很难阅读,导致内容对用户不可访问。请谨慎地少量使用非常小的文本。

将大小设置为百分比

从某种意义上说,百分比值与相对大小关键字非常相似。百分比值始终按照从元素父级继承的大小计算。与前面讨论过的大小关键字不同,百分比允许对计算出的字体大小进行更精细的控制。考虑下面的例子,图示在图 14-10 中。

body {font-size: 15px;}
p {font-size: 12px;}
em {font-size: 120%;}
strong {font-size: 135%;}
small, .fnote {font-size: 70%;}
<body>
<p>This paragraph contains both <em>emphasis</em> and <strong>strong
emphasis</strong>, both of which are larger than their parent element.
The <small>small text</small>, on the other hand, is smaller by a quarter.</p>
<p class="fnote">This is a 'footnote' and is smaller than regular text.</p>

<p> 12px <em> 14.4px </em> 12px <strong> 16.2px </strong> 12px
<small> 9px </small> 12px </p>
<p class="fnote"> 10.5px </p>
</body>

css5 1410

图 14-10. 将百分比混合使用

在此示例中,显示了确切的像素大小值。这些是浏览器计算的值,不考虑屏幕上字符的显示大小,可能已四舍五入到最接近的整数像素。

使用 em 测量时,同样的原则适用于百分比,如计算大小的继承等。CSS 将长度值 em 定义为与百分比值相等,即当调整字体大小时,1em 等同于 100%。因此,假设两个段落具有相同的父元素,则以下内容将产生相同的结果:

p.one {font-size: 166%;}
p.two {font-size: 1.66em;}

与相对大小关键字一样,百分比效果是累加的。因此,以下标记将按图 14-11 显示:

p {font-size: 12px;}
em {font-size: 120%;}
strong {font-size: 135%;}
<p>This paragraph contains both <em>emphasis and <strong>strong
emphasis</strong></em>, both of which are larger than the paragraph text. </p>

<p>12px <em>14.4px <strong> 19.44px </strong></em> 12px</p>

css5 1411

图 14-11. 继承的问题

<strong> 元素的大小值如图 14-11 所示计算如下:

  • 12 px × 120% = 14.4 px + 14.4 px × 135% = 19.44 px

缩放失控的问题也可能朝着另一个方向发展。如果我们嵌套了四层深的列表,想象以下规则对嵌套列表项的影响。

ul {font-size: 80%;}

嵌套四层深的无序列表将具有计算出的 font-size 值,为顶级列表的父元素大小的 40.96%。每个嵌套列表将具有比其父列表大 80% 的字体大小,导致每个级别变得越来越难阅读。

自动调整大小

影响字体可读性的两个主要因素是其大小和x-height,即字体中小写x字符的高度。将 x-height 除以font-size得到的数字称为aspect value。具有较高 aspect value 的字体在字体大小减小时保持可读性,相反,具有低 aspect value 的字体更快变得不可读。CSS 提供了处理字体族之间 aspect value 变化的方法,以及使用不同度量计算 aspect value 的方法,通过属性font-size-adjust

此属性的目标是在使用的字体不是作者首选字体时保持可读性。由于不同字体外观的差异,某种字体可能在特定大小下易读,而另一种字体在相同大小下可能难以或无法阅读。

属性值可以是nonefrom-font或数字。通常指定的数字应该是首选字体族的比例值(给定字体度量与字体大小的比率)。要选择用于计算比例值的字体度量,可以添加一个指定它的关键字。如果未包括,默认为ex-height,它通过将 x-height 除以字体大小来归一化字体的比例值。

字体度量关键字的其他可能性如下:

cap-height

使用字体的大写字母高度(大写字母的高度)。

ch-width

使用字体的水平间距(也是1ch的宽度)。

ic-width

使用 CJK 水象形字“水”(U+6C34)的字体宽度。

ic-height

使用 CJK 水象形字“水”(U+6C34)的字体高度。

声明font-size-adjust: none将抑制任何字体大小的调整。这是默认状态。

from-font关键字指示用户代理使用第一个可用字体的内置值的指定字体度量,而不要求作者找出该值并显式写入。因此,编写font-size-adjust: cap-height from-font将自动通过将 cap-height 除以 em-square 高度设置比例值。

一个好的例子是比较常见的字体 Verdana 和 Times。考虑图 14-12 和以下标记,显示了两种字体的font-size10px

p {font-size: 10px;}
p.cl1 {font-family: Verdana, sans-serif;}
p.cl2 {font-family: Times, serif; }

css5 1412

图 14-12. 比较 Verdana 和 Times

Times 中的文本比 Verdana 中的文本难以阅读得多。这部分是由于像素基础显示的限制,但也因为 Times 在较小的字体大小下变得更难阅读。

结果表明,Verdana 的 x-height 与字符大小的比率为 0.58,而 Times 为 0.46。为了使这些字体在视觉上更一致,可以声明 Verdana 的比例值,并让用户代理调整实际使用的文本大小。这是通过以下公式实现的:

  • 声明的font-size ×

    font-size-adjust值 ÷ 方面

    可用字体的值)= 调整后的font-size

因此,当使用 Times 而不是 Verdana 时,调整如下:

  • 10px × (0.58 ÷ 0.46) = 12.6px

这导致显示如图 14-13 所示的结果:

p {font: 10px Verdana, sans-serif; font-size-adjust: ex-height 0.58;}
p.cl2 {font-family: Times, serif; }

css5 1413

图 14-13. 调整时间

然而,用户代理要智能地进行大小调整,首先必须知道您指定的字体的方面值。支持@font-face的用户代理将能够直接从字体文件中提取该信息,假设文件包含这些信息——任何专业制作的字体应该包含,但不能保证。如果字体文件不包含方面值,则用户代理可能会尝试计算它;但同样,不能保证它们会或能够这样做。

如果用户代理无法找到或独立确定方面值,则font-size-adjustauto值是一种获得所需效果的方法,即使不知道首选字体的实际方面值也是如此。例如,假设用户代理可以确定 Verdana 的方面值为 0.58,则以下内容将与图 14-13 中显示的效果相同:

p {font: 10px Verdana, sans-serif; font-size-adjust: auto;}
p.cl2 {font-family: Times, serif; }
警告

截至 2022 年底,唯一支持font-size-adjust的用户代理系列是 Gecko(Firefox)系列。

当考虑size-adjust时,理解字体大小调整非常有用。这个字体描述符的行为与font-size-adjust属性类似,尽管它仅限于比较 x-height 而不是font-size-adjust可用的字体度量范围。

font-size-adjust属性是一个罕见的例子,其中属性和描述符名称并不相同:描述符是size-adjust。该值是任何正百分比值(从 0 到无穷大),您希望将备用字体缩放以更好地匹配所选的主字体。该百分比用作字形轮廓大小和字体的其他度量的乘数:

@font-face {
  font-family: myPreferredFont;
  src: url("longLoadingFont.otf");
}

@font-face {
  font-family: myFallBackFont;
  src: local(aLocalFont);
  size-adjust: 87.3%;
}
警告

截至 2022 年底,唯一不支持size-adjust描述符的用户代理系列是 WebKit(Safari)系列。

字体风格

font-style属性听起来非常简单:您可以从三个值中选择,并且如果使用斜体,则可以选择提供角度。

font-style的默认值是normal。这个值指的是直立文本,最好描述为既非斜体也非倾斜的文本。例如,本书中绝大部分文本都是直立的。

斜体字体通常外观上有些手写风格,并且通常比同一字体的normal版本使用更少的水平空间。在标准字体中,斜体文本是一个单独的字体面,每个字母的结构会因其改变的外观而略有变化。对于衬线字体尤为如此,因为除了文本字符“倾斜”外,还可能会改变衬线的形状。标有ItalicCursiveKursiv等标签的字体面通常与italic关键字相匹配。

倾斜文本 另一方面,是正常直立文本的倾斜版本。倾斜文本通常除了斜角之外不会改变直立文本。如果字体具有倾斜版本,则通常在标有ObliqueSlantedIncline等标签的字体中。

当字体没有斜体或倾斜版本时,浏览器可以通过人为倾斜常规面的字形来模拟斜体和倾斜字体。(为了防止这种情况发生,请使用font-synthesis: none,本章稍后会涉及。)

相同角度的斜体和倾斜文本并不相同:斜体是风格化的,并且通常经过精心设计,而倾斜只是简单地倾斜。默认情况下,如果声明oblique没有角度,将使用14deg的值。

当斜体指定角度,如font-style: oblique 25deg时,如果字体族中有一个或多个倾斜字体,则浏览器会选择被分类为倾斜的字体面。如果在所选字体族中有一个或多个倾斜字体面可用,则会选择最接近指定角度的字体面。如果没有可用的倾斜字体面,浏览器可以通过倾斜正常字体面指定的角度来合成一个倾斜版本。

除非受字体或描述符进一步限制,否则指定的倾斜角必须在90deg-90deg之间(包括端点)。如果给定的值超出这些限制,声明将被忽略。正值向行内结束方向倾斜,而负值向行内开始方向倾斜。

若要直观地展示斜体和倾斜文本的区别,请参考图 14-14。

css5 1414

图 14-14. 详细说明斜体和倾斜文本

对于 TrueType 或 OpenType 可变字体,使用"slnt"变化轴来实现不同的倾斜角度,使用"ital"变化轴并设置值为1来实现斜体。详见“字体变化设置”获取更多信息。

如果你希望确保文档中使用的斜体文本符合常规习惯,你可以编写类似以下样式表:

p {font-style: normal;}
em, i {font-style: italic;}

这些样式会使段落使用正常的直立字体,并导致<em><i>元素也使用斜体字体,这也是通常的做法。另一方面,您可能会决定<em><i>之间应有微妙的差异:

p {font-style: normal;}
em {font-style: oblique;}
i {font-style: italic;}
b {font-style: oblique -8deg;}

如果你仔细观察图 14-15,你会发现<em><i>元素之间看起来没有明显的区别。实际上,并非每种字体都如此复杂,能同时具备斜体和倾斜体,甚至更少的网页浏览器能够区分这两种字体。

css5 1415

图 14-15. 更多字体风格

italic的等效font-variation-settings设置是"ital"。对于oblique <*angle*>值,等效值是"slnt",用于在直立文本和倾斜文本之间进行变化。与font-style一样,倾斜轴被解释为从直立开始的逆时针角度:向内倾斜的倾斜设计将具有负倾斜值,而向外倾斜则需要正值。

字体风格描述符

作为描述符,font-style允许作者将特定字体与特定的font-style值关联起来。

例如,我们可能希望将特定的 Switzera 字体分配给各种font-style属性值。给定以下内容,结果将是使用 SwitzeraADF-Italic 而不是 SwitzeraADF-Regular 来呈现<h2><h3>元素,如图 14-16 所示:

@font-face {
   font-family: "Switzera";
   font-style: normal;
   src: url("SwitzeraADF-Regular.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-style: italic;
   src: url("SwitzeraADF-Italic.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-style: oblique;
   src: url("SwitzeraADF-Italic.otf") format("opentype");
}

h1, h2, h3 {font-family: SwitzeraADF, Helvetica, sans-serif;}
h1 {font-size: 225%;}
h2 {font-size: 180%; font-style: italic;}
h3 {font-size: 150%; font-style: oblique;}

css5 1416

图 14-16. 使用声明的字体风格

理想情况下,如果 SwitzeraADF 字体具有倾斜字体的面板,则页面作者可以指向它而不是斜体变体。但实际上并没有这样的字体,因此作者将斜体面板映射到了italicoblique值。与font-weight一样,font-style描述符可以采用font-style属性的所有值,但不能使用inherit

倾斜文本改变了字形的角度,而不进行任何字符替换。支持倾斜文本的任何可变字体也支持正常或直立文本:直立文本是倾斜角度为0deg的倾斜文本。例如:

@font-face {
  font-family: "varFont";
  src: url("aVariableFont.woff2") format("woff2-variations");
  font-weight: 1 1000;
  font-stretch: 75% 100%;
  font-style: oblique 0deg 20deg;
  font-display: swap;
}

body { font-family: varFont, sans-serif; font-style: oblique 0deg; }
em { font-style: oblique 14deg; }

在 CSS 值oblique 3deg中给出的角度是顺时针倾斜 3 度。正角度是顺时针倾斜,而负角度是逆时针倾斜。如果未包含角度,则与写oblique 14deg相同。度角可以是从-90deg90deg之间的任何值,包括这两个极端值。

字体拉伸

在某些字体系列中,变体字体具有较宽或较窄的字形。这些通常被称为 Condensed、Wide 和 Ultra Expanded。这些变体的实用性在于设计师可以在使用单一字体系列的同时,拥有细和粗的变体。CSS 提供了一个属性,允许作者在存在这些变体时选择其中之一,而无需在font-family声明中显式定义它们。它通过有些误导的font-stretch属性来实现这一点。

从属性名称中,您可能会期望 font-stretch 将像盐水软糖一样拉伸或挤压字体,但事实并非如此。该属性实际上更像 font-size 属性的绝对大小关键字(例如 `xx-large`)。您可以设置介于 50% 和 200% 之间的百分比,或使用具有定义百分比等效的一系列关键字值。表 14-9 显示了关键字值和数值百分比之间的映射关系。

表 14-9. font-stretch 关键字值的百分比等效

关键字 百分比
ultra-condensed 50%
extra-condensed 62.5%
condensed 75%
semi-condensed 87.5%
normal 100%
semi-expanded 112.5%
expanded 125%
extra-expanded 150%
ultra-expanded 200%

例如,您可能决定通过将字体字符更改为比其父元素字体字符更宽的面,来强调强调元素中的文本。

问题在于,此属性仅在使用的字体系列确实具有更宽和更窄的面时才有效,这些面大多数只随昂贵的传统字体提供。(它们在变量字体中更为广泛地提供。)

例如,考虑普通字体 Verdana,它只有一个宽度面;这等同于 font-stretch: normal。声明以下内容将不会对显示文本的宽度产生影响:

body {font-family: Verdana;}
strong {font-stretch: extra-expanded;}
footer {font-stretch: extra-condensed;}

所有文本都将保持 Verdana 的通常宽度。但是,如果将字体系列更改为具有多个宽度面的字体,例如 Futura,则会有所不同,如 图 14-17 所示:

body {font-family: Verdana;}
strong {font-stretch: extra-expanded;}
footer {font-stretch: extra-condensed;}

css5 1417

图 14-17. 拉伸字体字符

对于支持 "wdth" 轴的变量字体,将 font-variation-settings 中的宽度设置为大于 0 可控制字形宽度或笔画厚度,具体取决于字体设计。

字体拉伸描述符

就像 font-weight 描述符一样,font-stretch 描述符允许您将不同宽度的面显式分配给 font-stretch 属性中允许的宽度值。例如,以下规则显式地将三个面分配给最直接类似的 font-stretch 值:

@font-face {
   font-family: "Switzera";
   font-stretch: normal;
   src: url("SwitzeraADF-Regular.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-stretch: condensed;
   src: url("SwitzeraADF-Cond.otf") format("opentype");
}
@font-face {
   font-family: "Switzera";
   font-stretch: expanded;
   src: url("SwitzeraADF-Ext.otf") format("opentype");
}

与前几节类似,您可以通过 font-stretch 属性调用这些不同宽度面,如 图 14-18 所示:

h1, h2, h3 {font-family: SwitzeraADF, Helvetica, sans-serif;}
h1 {font-size: 225%;}
h2 {font-size: 180%; font-stretch: condensed;}
h3 {font-size: 150%; font-stretch: expanded;}

css5 1418

图 14-18. 使用声明的字体拉伸面

如果您使用的是包含完整字体拉伸尺寸范围的变量字体,可以通过 @font-face 导入单个字体文件,然后用于所有文本的字体拉伸需求。这将产生与 图 14-18 中显示的水平拉伸程度相同的效果,尽管使用的是不同的字体:

@font-face {
  font-family: 'League Mono Var';
  src: url('LeagueMonoVariable.woff2') format('woff2');
  font-weight: 100 900;
  font-stretch: 50% 200%;
  font-display: swap;
}

h1, h2, h3 {font-family: "League Mono Var", Helvetica, sans-serif;}
h2 {font-size: 180%; font-stretch: 75%;}
h3 {font-size: 150%; font-stretch: 125%;}

font-stretch 描述符可以采用 font-stretch 属性的所有值,除了 inherit

如果您希望根据文本是扩展还是压缩来使用不同的字体变体,可以在 @font-facefont-variation-settings 描述符的逗号分隔值中使用 "wdth" 值,如下例所示:

@font-face {
  font-family: 'League Mono Var';
  src: url('LeagueMonoVariable.woff2') format('woff2');
  font-weight: 100 900;
  font-stretch: 50% 200%;
}
strong {
  font-family: LeagueMono;
  font-variation-settings: "wdth" 100;
}

字体综合

有时,某个字体系列可能缺少粗体、斜体或小型大写字母的备用字体。在这种情况下,用户代理程序可能尝试从可用的字体中合成一个字体,但这可能导致字形不够吸引人。为解决此问题,CSS 提供了 font-synthesis,允许您控制页面渲染中的合成程度。这并没有 @font-face 描述符,但影响随后的所有字体变体,因此我们现在处理它。

在许多用户代理中,没有粗体面的字体系列可以为其计算一个。例如,这可以通过在每个字符字形的两侧添加像素来完成。尽管这可能看似有用,但在较小的字号下可能会导致视觉上不吸引人的结果。这就是为什么大多数字体系列都包含粗体面:字体设计者希望确保该字体的粗体文本看起来很好。

同样地,缺少斜体的字体系列可以通过简单地倾斜普通字体中的字符来合成一个斜体。这往往比合成粗体面更糟糕,尤其是在衬线字体方面。比较 Georgia 实际斜体面与合成斜体版本(我们在这里称为“斜体”),见图 14-19。

css5 1419

图 14-19. 综合斜体与设计斜体

在支持的用户代理中,声明 font-synthesis: none 可以阻止用户代理程序为受影响的元素进行任何合成。例如,您可以通过 html {font-synthesis: none;} 阻止整个文档的合成。缺点是,任何试图使用不提供适当字体面的字体创建变体文本的尝试都将保持普通面,而不是甚至近似预期结果。优点是,您不必担心用户代理程序试图合成这些变体并且做得很差。

字体变体

除了字重和字体样式之外,还有字体变体。这些嵌入在字体面中,可以涵盖历史连字的各种样式、小型大写字母的呈现方式、分数的呈现方式、数字的间距、零是否会被斜线穿过等方面。CSS 允许您在存在时通过简写属性 font-variant 调用这些变体。

此属性是五个单独属性的简写,我们马上就会详细讨论。您在实际使用中最常见的值是normal,这是默认值,用于描述普通文本,以及自 CSS1 以来存在的small-caps

不过,首先让我们介绍不与其他属性对应的两个值:

none

通过将font-feature-ligatures设置为none和所有其他字体变体属性设置为normal来禁用任何形式的所有变体

normal

通过将所有字体变体属性(包括font-feature-ligatures)设置为normal来禁用大多数变体

理解small-caps的变体方面可能有助于解释变体的概念,使得所有其他属性更容易理解。small-caps值要求使用小型大写字母(font-feature-settings: "smcp")。与大写和小写字母不同,小型大写字体使用不同大小的大写字母。因此,您可能会看到类似于图 14-20 中显示的内容:

h1 {font-variant: small-caps;}
h1 code, p {font-variant: normal;}
<h1>The Uses of <code>font-variant</code></h1>
<p>
The property <code>font-variant</code> is very interesting...
</p>

css5 1420

图 14-20. small-caps值的使用方式

正如您可能注意到的那样,在<h1>元素的显示中,源代码中的大写字母处有一个更大的大写字母,源代码中的小写字母处有一个小型大写字母。这与text-transform: uppercase非常相似,唯一的真正区别在于这里大写字母的大小不同。然而,将small-caps声明为字体属性的原因是一些字体具有特定的小型大写字体,可以使用字体属性进行选择。

如果不存在诸如small-caps之类的字体变体会发生什么?规范提供了两种选择。第一种是用户代理自行缩放大写字母以创建小型大写字母。第二种是将所有字母都变成大写并保持相同的大小,就像使用了声明text-transform: uppercase一样。这并不是一个理想的解决方案,但是是允许的。

警告

请注意,并非每种字体都支持每种变体。例如,大多数拉丁字体不支持任何东亚变体。此外,并非每种字体都包含对数字和连字变体的支持。许多字体将不支持任何变体。

要查看给定字体支持的内容,您必须查阅其文档,或者如果没有文档,则需要进行大量测试。大多数商业字体都附带有文档,而大多数免费字体则没有。幸运的是,一些浏览器开发者工具(截至 2022 年末,不包括 Chromium 浏览器)具有提供关于字体变体和特性设置信息的选项卡。

大写字体变体

除了我们刚刚讨论的small-caps值,CSS 还具有其他大写文本变体。这些通过属性font-variant-caps进行处理。

默认值为normal,表示不使用大写字母变体。从那里,我们有以下选项:

small-caps

使用大写字母渲染所有字母。源文本中为大写字母的字符的大写字母与大写字母的高度相同。文本中为小写字母的字符以较大的字母形式渲染,通常略高于字体的 x 高度。

all-small-caps

small-caps相同,但所有字母都以小型大写字母渲染,即使源文本中为大写字母。

petite-caps

small-caps类似,但是用于小写字母的大写字母的高度与字体的 x 高度相等,甚至稍矮一些。如果字体没有小型大写字母变体,结果可能与small-caps相同。

all-petite-caps

petite-caps相同,但所有字母都以小型大写字母渲染,即使源文本中为大写字母。

titling-caps

如果一行中有多个大写字母,交替使用大写形式以避免字母看起来过于视觉强烈。通常这些是字体中正常大写字母的较细版本。

unicase

文本使用大写和非大写字母形式的混合渲染,通常所有字母高度相同。即使在提供此变体的少数字体中,这种情况也可能差异很大。

下面的代码在图 14-21 中有所说明;请注意,用†标记的值在某种方式上是伪造的:

.variant1 {font-variant-caps: small-caps;}
.variant2 {font-variant-caps: all-small-caps;}
.variant3 {font-variant-caps: petite-caps;}
.variant4 {font-variant-caps: all-petite-caps;}
.variant5 {font-variant-caps: titling-caps;}
.variant6 {font-variant-caps: unicase;}

css5 1421

图 14-21。不同类型的大写字母变体

为什么在图 14-21 中我们伪造了一些示例?部分原因是因为找到一个包含所有大写字母变体的单一字体是极其困难的,而伪造一些结果比找到可能适用的字体或字体集要快得多。

我们还想要强调确切的情况:大多数情况下,你会得到一个回退(如从petite-capssmall-caps),或者根本没有变体。因此,请确保使用@font-face中的font-variant描述符来定义应该发生的情况。否则,如果没有可用的font-variant-caps类别变体,浏览器将决定如何呈现它。例如,如果指定了petite-caps并且字体没有 petite-caps 面或变量轴定义,用户代理可能会使用小型大写字母渲染文本。如果字体中不包括小型大写字母,浏览器可以通过等比例缩小大写字母来合成它们。

或者,您可以使用{font-synthesis: none;}来防止浏览器合成文本。您还可以包含{font-synthesis: small-caps;},或者完全省略font-synthesis,以允许在需要时合成小型大写字母字体。

字体有时会包含特殊的字形,用于各种无大小写字母的字符,比如标点符号,以匹配大写字母变体的文本。浏览器不会自行合成无大小写字母的字符。

除了 normal 外,所有 font-variant-caps 的值都有对应的 OpenType 特性。这些总结在 表格 14-10 中。

表格 14-10. font-variant-caps 的值和对应的 OpenType 特性

OpenType 特性
normal n/a
small-caps "smcp"
all-small-caps "c2sc", "smcp"
petite-caps "pcap"
all-petite-caps "c2pc", "pcap"
titling-caps "titl"
unicase "unic"

数字字体变体

许多字体在渲染数字时有不同的行为变体。如果可用,可以通过 font-variant-numeric 属性访问这些。此属性的值会影响数字、分数和序数标记的替代字形的使用。

默认值 normal 表示在渲染数字时不会进行任何特殊处理。它们将和字体的平常显示一样。图 14-22 展示了所有值,像之前一样,带有†标记的示例是由于字体缺乏这些特性而被伪造的。

css5 1422

图 14-22. 不同类型的数字变体

或许最简单的数值变体是 slashed-zero。这使得数字 0 带有一条斜杠,通常是斜对角的。在等宽字体中,斜杠零经常是默认渲染方式,因为区分数字 0 和大写字母 O 可能会很困难。在衬线和无衬线字体中,它们通常不是零的默认外观。设置 font-variant-numeric: slashed-zero 将显示斜杠零(如果有的话)。

谈到斜杠,值 diagonal-fractions 导致排列成分数形式的字符(例如 1/2)以较小的数字显示,第一个数字升高,通过斜杠分隔。值 stacked-fractions 以第一个数字在上,第二个数字在下的形式呈现分数,它们之间通过水平斜杠分隔。

如果字体具有序数标签的特性,例如英文中 1st、2nd、3rd 和 4th 后面的字母,ordinal 可以启用这些特殊字形的使用。它们通常看起来像上标、较小的字母版本。

作者可以通过 lining-nums 影响数字的形态,这将所有数字设置在基线上;而 oldstyle-nums 可以启用像 3、4、7 和 9 这样的数字下降到基线以下的形态。Georgia 是一个常见的具有旧样式数字的字体示例。

您还可以影响数字的尺寸。proportional-nums 值使数字成比例显示,就像比例字体一样;而 tabular-nums 则使所有数字具有相同的宽度,就像等宽字体一样。这些值的优势在于,假设字体中有支持它们的字形,您可以在比例字体中获得等宽效果,而不必将数字转换为等宽字体,同样地,也可以使等宽数字按比例大小显示。

您可以包含多个值,但每个数值集合中只能包含一个值:

@font-face {
  font-family: 'mathVariableFont';
  src: local("math");
  font-feature-settings: "tnum" on, "zero" on;
}
.number {
  font-family: mathVariableFont, serif;
  font-feature-settings: "tnum" on, "zero" on;
  font-variant-numeric: ordinal slashed-zero oldstyle-nums stacked-fractions;
}

除了 normal 以外的所有 font-variant-numeric 值都有定义的等效 OpenType 特性。这些总结在 表 14-11 中。

表 14-11. font-variant-numeric 值及其等效 OpenType 特性

Value OpenType feature
normal n/a
ordinal "ordn"
slashed-zero "zero"
lining-nums "lnum"
oldstyle-nums "onum"
proportional-nums "pnum"
tabular-nums "tnum"
diagonal-fractions "frac"
stacked-fractions "afrc"

连字变体

连字 是将两个(或更多)字符合并成一个形状。例如,两个小写字母 f 可以在相邻时将它们的横杠合并为一条线,或者横杠可以延伸到小写字母 i 上,替代其通常的点,形成 fi 序列。更古老的例子中,像 st 这样的组合可以用一种波状曲线将一个字母连接到另一个字母。在有支持时,这些特性可以通过 font-variant-ligatures 属性启用或禁用。

这些值具有以下影响:

common-ligatures

启用常见连字,如将 ft 与其后的字母组合起来。在法语中,oe 序列通常使用 œ 连字。浏览器通常默认启用这些功能,如果需要禁用它们,请改用 no-common-ligatures

discretionary-ligatures

启用字体设计师创建的特殊连字,这些连字不常见或以其他方式不被认为是常见的。

historical-ligatures

启用了历史连字的使用,这些连字通常出现在过去几个世纪的印刷术中,但今天已不再使用。例如,在德语中,tz 二字曾经以以下方式呈现:

contextual-ligatures

启用根据上下文出现的连字,例如手写字体可以根据前后字母不仅仅是后续字母的形状连接来渲染。这些特性有时也用于编程字体,例如序列 != 可能会被渲染为

no-common-ligatures

明确禁用常见连字。

no-discretionary-ligatures

明确禁用离散连字。

no-historical-ligatures

明确禁用历史连字的使用。

no-contextual-ligatures

明确禁用上下文连字的使用。

默认值normal关闭所有这些连字,除了默认情况下启用的常见连字。这特别重要,因为font-variant: normal关闭所有font-variant-ligatures,除了常见的连字,而font-variant: none关闭所有连字,包括常见连字。表 14-12 提供了每个值如何转换为 OpenType 功能的简要总结。

表 14-12. font-variant-ligatures值及其等效的 OpenType 功能

Value OpenType feature
common-ligatures "clig" on, "liga" on
discretionary-ligatures "dlig" on
historical-ligatures "hlig" on
contextual-ligatures "calt" on
no-common-ligatures "clig" off, "liga" off
no-discretionary-ligatures "dlig" off
no-historical-ligatures "hlig" off
no-contextual-ligatures "calt" off

较不可能被浏览器使用或支持的是font-variant-alternatesfont-variant-east-asian属性。

备选变体

对于任何给定字符,字体可能会包括除了该字符的默认字形外的备选字形。font-variant-alternates属性影响这些备选字形的使用。

默认值normal表示不使用任何备选变体。historical-forms关键字启用历史形式,即在过去常见但今天不常见的字形。所有其他值都是函数。

这些替代字形可能通过在@font-feature-values中定义的替代名称进行引用。通过@font-feature-values,您可以为font-variant-alternates函数值定义一个通用名称以激活 OpenType 功能。

@font-feature-values规则可以在 CSS 的顶级或任何 CSS 条件组中使用。

在表 14-13 中,XY将被代表一个表示特征集的数字替换。通过 OpenType 字体和font-feature-settings,一些功能已经定义。例如,styleset()函数的 OpenType 等效项是"ss*XY*"。截至 2022 年末,已经定义了ss01ss20。允许使用高于 99 的值,但它们不映射到任何 OpenType 值,并将被忽略。

表 14-13. font-variant-alternates值及其等效的 OpenType 功能

Value OpenType feature
annotation() "nalt"
character-variant() "cv*XY*"
historical-forms "hist"
ornaments() "ornm"
styleset() "ss*XY*"
stylistic() "salt"
swash() "swsh", "cswh"

font-variant-alternates@font-feature-values规则版本允许作者使用他们自己的规则定义font-variant-alternates的替代值的标签。以下两个样式(取自 CSS 规范)演示了如何标记swash替代的数字值,然后在font-variant-alternates中稍后使用它们:

@font-feature-values Noble Script { @swash { swishy: 1; flowing: 2; } }

p {
  font-family: Noble Script;
  font-variant-alternates: swash(flowing); /* use swash alternate #2 */
}

没有@font-feature-values规则的存在,段落样式必须使用font-variant-alternates: swash(2),而不是使用flowing作为swash函数值。

警告

截至 2022 年末,虽然所有浏览器都支持font-variant及其相关子属性,但只有 Firefox 和 Safari 支持font-variant-alternates@font-feature-values。您可以通过使用font-feature-settings属性来更可靠地设置这些变体。

东亚字体变体

font-variant-east-asian属性的值允许在东亚文本中控制字形替换和大小。

各种日本工业标准(JIS)变体反映了不同日本国家标准中定义的字形。字体通常包括最新国家标准定义的字形。当需要这些变体(例如复制历史文档时)时,JIS 值允许包括较旧的日文字形变体。

同样,simplifiedtraditional值允许控制随时间简化但在某些情况下仍使用旧的传统形式的字符的字形。

ruby值启用 Ruby 变体字形的显示。Ruby 文本通常比关联的正文文本小。

此属性值允许字体设计师包含更适合小型排版的字形,而不是默认字形的缩小版本。只影响字形选择;没有相关的字体缩放。

字体变体位置

与以前的变体相比,font-variant-position相对简单。然而,奇怪的是,它的支持却如此差。

此属性可用于启用专门为上标和下标文本设计的特殊变体字形。正如CSS 规范中所述,这些字形:

…设计在与默认字形相同的 em-box 中,并且旨在放置在与默认字形相同的基线上,没有重新调整或重新定位基线。它们明确设计为与周围文本匹配,并且更易阅读,而不影响行高。

这与缺乏这种替代形式的字体中的上标和下标文本的情况恰恰相反,通常只是从基线向上或向下移动的较小文本。这种超级和下标文本的合成通常会导致行高增加,而变体字形通常设计为防止这种情况发生。

字体特性设置

在本章中,我们已经讨论了字体特性,但尚未涉及 font-feature-settings 属性或描述符。与 font-variant 类似,font-feature-settings 允许您对可用的 OpenType 字体特性进行低级控制。

font-feature-settings 属性控制 OpenType 字体中的高级排版特性,而不是 font-variation-settings 属性,后者提供对可变字体特性的低级控制。

您可以列出一个或多个逗号分隔的 OpenType 特性,如 OpenType 规范所定义。例如,启用常见连字、小型大写字母和斜线零的方法如下所示:

font-feature-settings: "liga" on, "smcp" on, "zero" on;

<特性标签值> 值的确切格式如下:

<特性标签值>

<字符串> [ <整数> | | ]?

对于许多特性,唯一允许的整数值是 01,它们分别等同于 (反之亦然)。然而,有些特性允许一系列数字,此时大于 1 的值既启用该特性又定义该特性的选择索引。如果列出了一个特性但没有提供数字,则假定为 1(开)。因此,以下描述符都是等效的:

font-feature-settings: "liga";     /* 1 is assumed */
font-feature-settings: "liga" 1;   /* 1 is declared */
font-feature-settings: "liga" on;  /* on = 1 */

请记住,所有 <字符串> 值 必须 用引号括起来。因此,以下描述符中的第一个将被识别,而第二个将被忽略:

font-feature-settings: "liga", dlig;
/* common ligatures are enabled; we wanted discretionary ligatures, but forgot
 quotes, so they are not enabled */

另一个限制是 OpenType 要求所有特性标签均为四个 ASCII 字符长。任何长度超过或不足四个字符,或使用非 ASCII 字符的特性名称都是无效的,将被忽略。(除非您使用的字体具有自己创造的特性名称,而字体创建者未遵循命名规则,否则您无需担心此问题。)

默认情况下,OpenType 字体除非作者通过 font-feature-settingsfont-variant 明确禁用,否则始终启用以下特性:

"calt"

上下文替换

"ccmp"

复合字符

"clig"

上下文连字

"liga"

标准连字

"locl"

本地化形式

"mark"

基于基准位置的定位

"mkmk"

基于标记的定位

"rlig"

必需的连字

此外,在特定情况下,默认情况下可能启用其他特性,例如文本的垂直替代 ("vert")。

到目前为止,我们讨论的所有 OpenType font-feature-setting 值都在 表 14-14 中列出,还有一些由于缺乏支持而未涉及的其他值。

表 14-14. OpenType 值

代码 含义 详细写法
"afrc" 替代分数 stacked-fractions
"c2pc" 小型大写字母 petite-caps
"c2sc" 大写字母的小型大写字母 all-small-caps
"calt" 上下文替换 contextual
"case" 区分大小写形式
"clig" 常见连字 common-ligatures
"cswh" 花体功能 swash()
"cv01" 字符变体 (01–99) character-variant()
"dnom" 分母
"frac" 分数 diagonal-fractions
"fwid" 全宽变体 full-width
"hist" 启用历史形式 historical-forms
"liga" 标准连字 common-ligatures
"lnum" 直线数字 lining-nums
"locl" 本地化形式
"numr" 分子
"nalt" 注释功能 annotation()
"onum" 古老数字 oldstyle-nums
"ordn" 序数标记 ordinal
"ornm" 装饰物(功能) ornaments()
"pcap" 小型大写字母 petite-caps
"pnum" 比例数字
"pwid" 按比例空格的变体 proportional-width
"ruby" 注音 ruby
"salt" 风格功能 stylistic()
"sinf" 科学下标
"smcp" 小型大写字母 small-caps
"smpl" 简化形式 simplified
"ss01" 风格设置 1(正确的编号) styleset()
"ss07" 风格设置(1–20) styleset()
"subs" 下标
"sups" 上标
"swsh" 花体功能 swash()
"titl" 标题大写字母 titling-caps
"tnum" 制表数字 tabular-nums
"trad" 传统形式 traditional
"unic" 单例 unicase
"zero" 斜线零 slashed-zero

标准 OpenType 特性名称的完整列表可以在Microsoft 的注册特性页面上找到。

话虽如此,font-feature-settings是一种低级功能,旨在处理除启用或访问 OpenType 字体特性之外没有其他方法处理的特殊情况。你还必须在单个属性值中列出所有要使用的特性设置。在可能的情况下,请使用font-variant简写属性或六个相关的分开的属性:font-variant-ligaturesfont-variant-capsfont-variant-east-asianfont-variant-alternatesfont-variant-positionfont-variant-numeric

font-feature-settings描述符

font-feature-settings描述符让您决定使用 OpenType 字体面的哪些设置可以或不能使用,指定为以空格分隔的列表。现在,等一下—难道这几乎不就是我们刚刚在几个段落前使用font-variant做的吗?是的!font-variant描述符几乎包括font-feature-settings所做的一切,再加上一点额外的功能。只是以一种更像 CSS 的方式,通过值名称而不是神秘的 OpenType 标识符和布尔切换来实现。正因为如此,CSS 规范明确鼓励作者使用font-variant而不是font-feature-settings,除非font-variant的值列表中没有包括某个字体特性。

请记住,此描述符仅使功能可供使用(或抑制其使用)。它不会为文本的显示打开它们;有关详细信息,请参阅“字体特征设置”。

font-variant 描述符一样,font-feature-settings 描述符定义了在 @font-face 规则中声明的字体面的哪些字体功能已启用(或禁用)。例如,给定以下内容,即使 SwitzeraADF 中存在这些功能,Switzera 将禁用替代分数和小型大写字母:

@font-face {
  font-family: "Switzera";
  font-weight: normal;
  src: url("SwitzeraADF-Regular.otf") format("opentype");
  font-feature-settings: "afrc" off, "smcp" off;
}

font-feature-settings 描述符可以接受 font-feature-settings 属性的所有值,除了 inherit

字体变体设置

font-variation-settings 属性通过指定四个字母的轴名称和值,提供对可变字体特性的低级控制。

有五个注册轴,列在表 14-15 中。我们已经涵盖了几乎所有的内容。

表 14-15. 字体变体轴

属性 属性值
"wght" font-weight 11000
"slnt" font-style oblique / oblique
"ital" font-style italic
"opsz" font-optical-sizing
"wdth" font-stretch

我们使用术语注册轴,因为字体开发者不仅限于重量、宽度、光学大小、倾斜和斜体:他们可以创建自定义轴,并通过提供四个字母的标签“注册” 它们。要知道字体是否具有此类轴的最简单方法是查看字体的文档;否则,您必须知道如何深入查看字体文件的内部以了解详情。这些轴可以控制字体外观的任何方面,例如小写字母ij上点的大小。创建自定义轴超出了本书的范围,但在存在这些轴的地方调用它们是可行的。

由于这些轴是字符串值,因此必须用引号引起来,大小写敏感,并始终为小写。想象一个字体,其中小写字母ij上的点(正确称为变音符号或简称变音符号)可以通过称为 DCSZ变音符号大小)的轴来更改。此外,该轴已由字体设计师定义,允许值从 1 到 10。变音符号大小可以最大化如下:

p {font-family: DotFont, Helvetica, serif; font-variation-settings: "DCSZ" 10;}

font-variation-settings 描述符与属性相同。与单独声明每个注册轴不同,它们在一行上声明,用逗号分隔:

@font-face {
  font-family: 'LeagueMono';
  src: url('LeagueMonoVariable.woff2') format('woff2');
  font-weight: 100 900;
  font-stretch: 50% 200%;
  font-variation-settings: 'wght' 100 900, 'wdth' 50 200;
  font-display: swap;
}
提示

尽管您可以使用 font-variation-settings 设置给定字体的重量、样式等,但建议您改用更广泛支持且人类可读的属性 font-weightfont-style

字体光学大小

在不同大小的文本上渲染的文本通常会从略微不同的视觉表现中受益。例如,为了辅助小文本大小的阅读,字形的细节较少,笔划通常更粗,带有更大的衬线。较大的文本可以具有更多的特征和更大的粗细笔划对比。font-optical-sizing属性允许作者启用或禁用变体字体的此功能。

默认情况下(通过auto),浏览器可以根据字体大小和像素密度修改字形的形状。none值告诉浏览器不要这样做。

提示

在支 在支持的字体中,光学大小通常定义为一系列数字。如果你想明确将某个元素字体的光学大小更改为特定数字,或许是为了使文本比默认状态下更加坚固或精致,使用font-variation-settings属性并赋值如 'opsz' 10(其中10可以是光学大小范围内的任何数字)。

覆盖描述符

这使我们进入了我们还未讨论的最后三个@font-face描述符。三个描述符启用字体系列的覆盖设置:ascent-overridedescent-overrideline-gap-override,分别定义上升度量、下降度量和行间距度量。所有三个描述符的取值相同:normal或<percentage>。

这些描述符的目标是通过覆盖备用字体的度量值并使用主字体的度量值来帮助备用字体更好地匹配主字体。

上升度量是用于布局行框的基线之上的距离(基线到 em 框顶部的距离)。下降度量是用于布局行框的基线之下的距离(基线到 em 框底部的距离)。行间距度量是字体推荐的相邻文本行之间的距离,有时称为外部行距

下面是一个假设字体及其上升度量、下降度量和行间距覆盖描述符的示例:

@font-face {
  font-family: "PreferredFont";
  src: url("PreferredFont.woff");
}

@font-face {
  font-family: FallbackFont;
  src: local(FallbackFont);
  ascent-override: 110%;
  descent-override: 95%;
  line-gap-override: 105%;
}

这将指示浏览器将上升和下降高度分别调整为 110%和 95%,并将行间距增加到备用字体的距离的 105%。

字体字距

一个没有等效描述符的字体属性是font-kerning。一些字体包含数据,指示字符之间的间距,称为字距。字距可以使字符间距更加美观且易于阅读。

字距空间因字符组合方式而异;例如,字符对oc的间距可能与字符对ox不同。类似地,ABAW的分隔距离也可能不同,以至于在某些字体中,W的右上角尖端实际上放置在A的右下角尖端的左侧。这些字距数据可以通过font-kerning属性显式调用或抑制。

none 的值非常简单:它告诉用户代理忽略字体中的任何字距信息。normal 值告诉用户代理按照字体中包含的字距数据正常进行字距处理。auto 值告诉用户代理按照它认为最好的方式进行处理,可能取决于所使用的字体类型。例如,OpenType 规范建议(但不要求)在字体支持时应用字距。此外,根据CSS 规范

[浏览器] 可能会使用包含kern表中的字距数据但缺少GPOS表中字距特性支持的字体,合成地支持字距特性。

这意味着,实际上,如果字体中内置了字距信息,浏览器可以在没有通过特征表显式启用字距的情况下强制执行它。

注意

如果对经过字距处理的文本应用letter-spacing属性(参见第 15 章),则首先进行字距处理,然后根据letter-spacing的值调整字母间距,而不是反过来。

字体属性

到目前为止讨论的所有属性都非常复杂,但是把它们全部写出来可能会有点乏味:

h1 {font-family: Verdana, Helvetica, Arial, sans-serif; font-size: 30px;
    font-weight: 900; font-style: italic; font-variant-caps: small-caps;}
h2 {font-family: Verdana, Helvetica, Arial, sans-serif; font-size: 24px;
    font-weight: bold; font-style: italic; font-variant-caps: normal;}

通过组合选择器可以解决部分问题,但将所有内容合并为单个属性会更简单吗?这就是font的作用,它是一个包含大多数(并非全部)其他字体属性的简写属性,还有一些其他内容。

一般来说,font 声明可以从列出的每个字体属性中选择任何一个值,或者是系统字体值(在“使用系统字体”中描述)。因此,前面的例子可以缩短如下(并且效果完全相同,如图 14-23 所示):

h1 {font: italic 900 small-caps 30px Verdana, Helvetica, Arial, sans-serif;}
h2 {font: bold normal italic 24px Verdana, Helvetica, Arial, sans-serif;}

css5 1423

图 14-23. 典型的字体规则

我们说样式可以以这种方式“缩短”,是因为font的书写方式比较宽松,所以还有其他几种可能性。如果你仔细观察前面的例子,你会发现前三个值的顺序并不相同。在 h1 规则中,前三个值是 font-stylefont-weightfont-variant,依此顺序。而在第二个例子中,它们的顺序是 font-weightfont-variantfont-style。这里没有问题,因为这三个值可以按任意顺序书写。此外,如果其中任何一个值是 normal,那么可以完全省略它。因此,以下规则等同于前一个例子:

h1 {font: italic 900 small-caps 30px Verdana, Helvetica, Arial, sans-serif;}
h2 {font: bold italic 24px Verdana, Helvetica, Arial, sans-serif;}

在这个例子中,normal 的值被省略在 h2 规则中,但效果与前面的例子完全相同。

然而,重要的是要意识到,这种自由情况仅适用于font的前三个值。后两个值的行为要严格得多。font-sizefont-family必须以这个顺序作为声明的最后两个值出现,而且在font声明中必须始终同时出现。如果任何一个被省略,整个规则将无效,并且用户代理将完全忽略它。因此,以下规则将给您带来图 14-24 中显示的结果:

h1 {font: normal normal italic 30px sans-serif;}   /* no problem here */
h2 {font: 1.5em sans-serif;}   /* also fine; omitted values set to 'normal' */
h3 {font: sans-serif;}     /* INVALID--no 'font-size' provided */
h4 {font: lighter 14px;}   /* INVALID--no 'font-family' provided */

css5 1424

图 14-24. 大小和字族的必要性

理解字体属性的限制

因为font属性自 CSS 诞生以来就存在,并且因为后来引入了许多处理所有变体的属性,所以在涉及字体变体时,font属性存在一些限制。

首先,重要的是记住,当使用font简写属性时,尽管不能在font中表示,以下属性都将设置为其默认值:

  • font-feature-settings

  • font-kerning

  • font-language-override

  • font-optical-sizing

  • font-palette

  • font-size-adjust

  • font-variant-alternates

  • font-variant-caps(除非font值中包含small-caps

  • font-variant-east-asian

  • font-variant-ligatures

  • font-variant-numeric

  • font-variation-settings

第二点,延续前面列表中的说明,font属性只允许两个变体值:small-capsnormal。不能通过font属性设置数字、连字、备用字形、东亚字体等多种变体。例如,如果您希望在顶级标题中使用小型大写字母和斜线零,则需要编写如下内容:

h1 {font: bold small-caps 3em/1.1 Helvetica, sans-serif;
    font-variant-numeric: slashed-zero;

第三点,受历史沉淀影响的属性值是字体拉伸。正如我们在本章前面讨论的那样,font-stretch允许您从多个关键字中选择,或在 50%到 200%(包括边界)的范围内设置百分比。关键字可以在font中使用,但百分比值则不能。

添加行高

我们还可以通过font设置line-height属性的值,即使line-height是一个文本属性(本章不涉及),而不是字体属性。这是通过斜线(/)与font-size值分隔开来实现的:

body {font-size: 12px;}
h2 {font: bold italic 200%/1.2 Verdana, Helvetica, Arial, sans-serif;}

这些规则在图 14-25 中展示,将所有<h2>元素设置为粗体和斜体(使用一种无衬线字体族),将font-size设置为24px(是body大小的两倍),并将line-height设置为28.8px

css5 1425

图 14-25. 将行高添加到混合中

对于line-height的添加是完全可选的,就像前三个font值一样。如果包括line-height值,请记住,font-size始终位于line-height之前,而不是之后,并且两者之间始终用斜杠分隔。

警告

这可能看起来有些重复,但这是 CSS 作者经常犯的最常见错误之一,所以我们不能说得太多:font的必需值是font-sizefont-family,按照这个顺序。其他所有内容都是严格可选的。

正确使用简写

重要的是要记住,font作为简写属性,如果使用不慎可能会产生意想不到的效果。请考虑以下规则,这些规则在图 14-26 中有示例:

h1, h2, h3 {font: italic small-caps 250% sans-serif;}
h2 {font: 200% sans-serif;}
h3 {font-size: 150%;}
<h1>A level 1 heading element</h1>
<h2>A level 2 heading element</h2>
<h3>A level 3 heading element</h3>

css5 1426

图 14-26。简写变更

你是否注意到<h2>元素既不是斜体,也不是小型大写字母,并且所有元素都不是粗体?这是正确的行为。当使用简写属性font时,任何省略的值都会被重置为它们的默认值。因此,前面的示例也可以写成以下形式,仍然完全等效:

h1, h2, h3 {font: italic normal small-caps 250% sans-serif;}
h2 {font: normal normal normal 200% sans-serif;}
h3 {font-size: 150%;}

这将设置<h2>元素的字体样式和变体为normal,并将所有三个元素的font-weight设置为normal。这是简写属性的预期行为。<h3>不会像<h2>那样受到影响,因为你使用了font-size属性,它不是简写属性,因此仅影响其自身的值。

使用系统字体

当您希望网页与用户操作系统融为一体时,系统字体的font值非常有用。这些值用于获取操作系统元素的字体大小、系列、粗细、样式和变体,并应用于一个元素。这些值如下:

caption

用于带标题的控件,如按钮

icon

用于标记图标

menu

用于菜单——即下拉菜单和菜单列表

message-box

用于对话框

small-caption

用于标记小控件

status-bar

用于窗口状态栏

例如,您可能希望将按钮的字体设置为与操作系统中的按钮相同。例如:

button {font: caption;}

使用这些值,您可以创建看起来非常像用户操作系统中本地应用程序的基于 Web 的应用程序。

请注意,系统字体只能作为整体设置;即字体系列、大小、粗细、样式等都一起设置。因此,与之前示例中的按钮文本相比,不管大小是否与按钮周围的内容匹配,按钮文本都将完全相同。但是,在设置系统字体后,您可以更改各个值。因此,以下规则将确保按钮的字体与其父元素的字体大小相同:

button {font: caption; font-size: 1em;}

如果调用系统字体而用户机器上不存在这样的字体,则用户代理可以尝试找到一个近似值,比如将caption字体的大小减小到small-caption字体。如果无法找到这样的近似值,则用户代理应该使用自己的默认字体。如果可以找到系统字体但无法读取其所有值,则应使用默认值。例如,用户代理可能能够找到status-bar字体但无法获取有关该字体是否为小型大写字母的信息。在这种情况下,用户代理将为small-caps属性使用normal值。

字体匹配

正如你所见,CSS 允许匹配字体系列、字重和变体。这一切都通过字体匹配完成,这是一个略微复杂的过程。对于希望帮助用户代理在显示其文档时进行良好字体选择的作者来说,理解这一过程非常重要。我们将它放在章节末尾,因为理解字体属性的工作原理并不是必需的,而且一些读者可能会跳过这部分。如果你仍然感兴趣,下面是字体匹配的工作原理:

  1. 用户代理创建或访问一个字体属性数据库。该数据库列出用户代理可以访问的所有字体的各种 CSS 属性。通常,这将是安装在机器上的所有字体,尽管可能还有其他字体(例如,用户代理可能具有其自己内置的字体)。如果用户代理遇到两个相同的字体,它将忽略其中一个。

  2. 用户代理解析应用了字体属性的元素,并构建一个显示该元素所需的字体属性列表。根据该列表,用户代理首先选择要在显示该元素时使用的字体系列。如果完全匹配,用户代理可以使用该字体。否则,用户代理需要做更多工作。

  3. 首先匹配font-stretch属性。

  4. 接下来匹配font-style属性。关键词italic匹配任何标记为italicoblique的字体。如果两者都不可用,则匹配失败。

  5. 接下来匹配font-weight,由于 CSS 中处理font-weight的方式(在“字重工作原理”中解释),它永远不会失败。

  6. 然后处理font-size。必须在一定容差内匹配,但该容差由用户代理定义。因此,一个用户代理可能允许在指定大小和实际使用大小之间有 20%的误差范围,而另一个用户代理可能仅允许 10%的差异。

  7. 如果在第 2 步中没有匹配到字体,则用户代理会查找同一字体系列中的备用字体。如果找到任何备用字体,则对该字体重复第 2 步。

  8. 假设已经找到了一个通用匹配项,但不包含显示给定元素所需的全部内容——例如,字体缺少版权符号——用户代理会回到步骤 3,这包括搜索另一个备选字体并再次进行步骤 2。

  9. 最后,如果没有找到匹配项,并且已经尝试了所有备选字体,用户代理会为给定的通用字体系列选择默认字体,并尽力正确显示元素。

此外,用户代理还执行以下操作以解决字体变体和特征的处理:

  1. 检查默认启用的字体特征,包括特定脚本所需的特征。默认启用的核心特征集包括 "calt""ccmp""clig""liga""locl""mark""mkmk""rlig"

  2. 如果字体是通过 @font-face 规则定义的,请检查 @font-face 规则中 font-variant 描述符隐含的特征。然后检查 @font-face 规则中 font-feature-settings 描述符隐含的字体特性。

  3. 检查由除了 font-variantfont-feature-settings 之外的属性确定的特征设置。(例如,为 letter-spacing 属性设置非默认值将禁用连字。)

  4. 检查由 font-variant 属性的值、相关的 font-variant 子属性(例如 `font-variant-ligatures`)以及可能需要使用 OpenType 特性的任何其他属性隐含的特征。

  5. 检查由 font-feature-settings 属性的值隐含的特征。

整个过程冗长而乏味,但有助于理解用户代理如何选择它们所选择的字体。例如,您可以指定在文档中使用 Times 或任何其他衬线字体:

body {font-family: Times, serif;}

对于每个元素,用户代理应检查该元素中的字符,并确定 Times 是否能够提供匹配的字符。在大多数情况下,它可以毫无问题地做到这一点。

但假设在段落中间放置了一个中文字。Times 没有任何可以匹配此字符的东西,因此用户代理必须解决该字符的显示需求或寻找另一个能够满足显示该元素需求的字体。任何西文字体几乎不可能包含中文字,但如果存在(我们称其为 AsiaTimes),用户代理可以在显示该元素时使用它——或仅用于单个字符。因此,整个段落可能使用 AsiaTimes 显示,或者段落中的所有内容都是 Times,除了单个中文字符以外,该字符使用 AsiaTimes 显示。

总结

从最初非常简单的字体属性集合,CSS 已经发展到允许在 web 上对字体显示方式进行精细和广泛的影响。从通过 web 下载的自定义字体到由各种单独字体组合而成的自定义族,作者可以说是在字体力量中充满溢出。

今天作者可以使用的排版选项比以往任何时候都更为强大,但请记住:你必须明智地使用这种力量。尽管你可以在网站上使用 17 种字体,但这绝对不意味着你应该这样做。除了这可能给用户带来审美困难之外,它还会使页面的总重量远远超过必要的程度。就像网页设计的任何其他方面一样,建议你明智地使用你的权力,而不是肆意地使用。

第十五章:文本属性

因为文本如此重要,许多 CSS 属性在一定程度上影响它。但我们在第十四章只是介绍了字体的部分。不完全一样:我们只是讨论了字体——引入和使用字体。文本样式是不同的。

那么,文本和字体之间有什么区别?在最简单的层面上,文本是内容,字体用于显示该内容。字体提供了字母的形状。文本是围绕这些形状的样式。使用文本属性,你可以影响文本与行的其余部分的位置关系,上标、下划线和更改大小写。你可以影响文本装饰的大小、颜色和位置。

缩进和内联对齐

让我们先讨论一下如何影响文本在一行内的内联定位。可以把这些基本操作看作是创建通讯或撰写报告的步骤。

最初,CSS 是基于水平垂直的概念。为了更好地支持所有语言和写作方向,CSS 现在使用术语块方向内联方向。如果你的母语是西方语言衍生的,你习惯于从上到下的块方向和从左到右的内联方向。

块方向是在当前书写模式中块元素的默认放置方向。例如,在英语中,块方向是从上到下的,即垂直方向,一个段落(或其他文本元素)位于前一个段落的下方。有些语言有垂直文本,比如蒙古语。当文本是垂直的时,块方向是水平的。

内联方向是内联元素在块内书写的方向。以英语为例,内联方向是从左到右,即水平方向。在阿拉伯语和希伯来语等语言中,内联方向则是从右到左。再以前面段落中的例子来说明,蒙古语的内联方向是从上到下。

让我们再考虑一下英语。在屏幕上显示的普通英文页面具有垂直的块方向(从上到下)和水平的内联方向(从左到右)。但是,如果通过使用 CSS 转换使页面逆时针旋转 90 度,突然之间块方向是水平的,而内联方向是垂直的(而且是从下到上的)。

提示

你在网络上仍然可以找到许多关于写作方向的英文中心博客文章和其他与 CSS 相关的文档,使用垂直水平这些术语。在必要时,将它们心理转化为内联

文本缩进

大多数西方语言的纸质书籍的文字段落格式为首行缩进,段落之间没有空行。如果你想重新创建这种外观,CSS 提供了text-indent属性。

使用 text-indent,可以缩进任何元素的第一行,即使长度为负数。这个属性的常见用法是缩进段落的第一行:

p {text-indent: 3em;}

这条规则将导致任何段落的第一行缩进 3 个 ems,如图 15-1 所示。

css5 1501

图 15-1. 文本缩进

通常情况下,您可以将 text-indent 应用于生成块级盒子的任何元素,缩进将沿着内联方向发生。您不能将其应用于内联元素或替换元素(如图片)。但是,如果在块级元素的第一行中有一张图片,该图片将会随着文本一起向右移动。

注意

如果您想“缩进”内联元素的第一行,可以通过左内边距或外边距来实现这种效果。

您也可以为 text-indent 设置负值,以创建悬挂缩进,其中第一行超出其余元素的一侧:

p {text-indent: −4em;}

当设置 text-indent 的负值时,请注意,如果不小心,前几个单词可能会被浏览器窗口的边缘截断。为避免显示问题,建议您使用外边距或内边距来适应负缩进:

p {text-indent: −4em; padding-left: 4em;}

可以使用任何长度单位,包括百分比值,与 text-indent 一起使用。在以下情况下,百分比是指被缩进元素的父元素宽度。换句话说,如果将缩进值设置为 10%,则受影响元素的第一行将缩进其父元素宽度的 10%,如图 15-2 所示:

div {width: 400px;}
p {text-indent: 10%;}
<div>
<p>This paragraph is contained inside a DIV, which is 400px wide, so the
first line of the paragraph is indented 40px (400 * 10% = 40).  This is
because percentages are computed with respect to the width of the element.</p>
</div>

css5 1502

图 15-2. 百分比文本缩进

请注意,由于 text-indent 是继承的,一些浏览器(如 Yandex 浏览器)会继承计算值,而 Safari、Firefox、Edge 和 Chrome 则继承声明的值。在以下示例中,因为 5em 的值在 Yandex 和旧版本的 WebKit 中从其父级 <div> 继承到段落中,所以在 Yandex 中两段文本都将缩进 5 个 em,而在其他浏览器中,文本将按当前元素宽度的 10% 缩进:

div#outer {width: 50em;}
div#inner {text-indent: 10%;}
p {width: 20em;}
<div id="outer">
<div id="inner">
This first line of the DIV is indented by 5em.
<p>
This paragraph is 20em wide, and the first line of the paragraph
is indented 5em in WebKit and 2em elsewhere.  This is because
computed values for 'text-indent' are inherited in WebKit,
while the declared values are inherited elsewhere.
</p>
</div>
</div>

截至 2022 年末,有两个关键词正在考虑添加到 text-indent 中:

hanging

反转缩进效果;即 text-indent: 3em hanging 会缩进除第一行之外的所有文本行。这类似于之前讨论过的负值缩进,但不会因为将第一行拉出内容框而导致文本被截断,因为除第一行外的所有行都会从内容框的边缘缩进。

each-line

缩进元素的第一行,以及由 <br> 引起的强制换行后开始的任何行,但不包括跟随软换行的行。

在支持的情况下,可以与长度或百分比一起使用,例如:

p {text-indent: 10% hanging;}
pre {text-indent: 5ch each-line;}

对齐文本

text-indent 更基础的是 text-align 属性,它影响元素内文本行的相互对齐方式。

理解这些值如何工作的最快方法是查看 图 15-3,展示了最常使用的值。leftrightcenter 这些值会使元素内的文本在水平语言(如英语或阿拉伯语)中完全按照这些词描述的方式对齐,无论语言的内联方向如何。

css5 1503

图 15-3. text-align 属性的选定行为

text-align 的默认值是 start,在从左到右(LTR)的语言中等同于 left,在从右到左(RTL)的语言中等同于 right。在竖排文字中,leftright 分别映射到起始或结束边缘。详见 图 15-4。

因为 text-align 仅适用于段落等块级元素,因此没有办法在其行内居中锚点而不影响其余文本行(也不应该这样做,因为那可能导致文本重叠)。

如您所料,center 使文本每行居中于元素内。如果您曾遇到过早已废弃的 <CENTER> 元素,则可能认为 text-align: center 与其相同。实际上它们有很大不同。<CENTER> 元素不仅影响文本,还居中整个元素,比如表格。而 text-align 属性仅控制内联内容的对齐,不影响元素的对齐。

css5 1504

图 15-4. 垂直书写模式中的左、右和中心对齐

起始和结束对齐

记住 CSS 最初是基于 水平垂直 概念,初始默认值曾是“一个无名值,若 directionltr,则表现为 left,若 directionrtl,则表现为 right”。现在默认值已有名字:start,在从左到右(LTR)的语言中等同于 left,在从右到左(RTL)的语言中等同于 right

start 的默认值意味着文本对齐到其行框的起始边缘。在英语等从左到右(LTR)的语言中,这是左边缘;在阿拉伯语等从右到左(RTL)的语言中,这是右边缘。在竖排文字中,这将是顶部或底部,具体取决于书写方向。总之,该默认值在意识到文档语言方向的同时,保持了现有情况中的默认行为一致。

类似地,end 使文本与每个行框的结束边缘对齐 —— 在从左到右(LTR)的语言中是右边缘,在从右到左(RTL)的语言中是左边缘,以此类推。图 15-5. 展示了这些值的效果。

css5 1505

图 15-5. 起始和结束对齐

两端对齐的文本

常被忽视的对齐值是 justify,它本身引发了一些问题。在两端对齐的文本中,一行文本的两端(除了最后一行,可以使用 text-align-last 设置)放置在父元素的内边缘,如 图 15-6 所示。然后,调整单词和字母之间的间距,使单词均匀分布在行中。两端对齐的文本在印刷界很常见(例如在本书中),但在 CSS 下,还有一些额外的考虑因素。

css5 1506

图 15-6. 两端对齐的文本

用户代理确定如何拉伸或分配两端对齐文本以填充父元素的左右边缘之间的空间。例如,某些浏览器可能只在单词之间添加额外空间,而其他浏览器可能在字母之间分配额外空间(尽管 CSS 规范规定如果属性 letter-spacing 被指定为长度值,则“用户代理可能不会进一步增加或减少字符间距”)。其他用户代理可能会在某些行上减少空间,从而使文本比通常更紧凑。

justify-all 设置了 text-aligntext-align-last 的完全两端对齐(在即将介绍的部分中涵盖)。

警告

截至 2022 年中期,即使几乎所有浏览器都支持 text-align: justifytext-align-last: justify,但是没有任何浏览器支持 justify-all 值。截至出版时仍然存在这种支持差距,但在大多数浏览器中通过以下方式解决:

.justify-all {
  text-align: justify;
  text-align-last: justify;
  }

父匹配

我们还有一个值需要介绍:match-parent。如果声明 text-align: match-parent,并且 text-align 的继承值为 startend,则 match-parent 元素的对齐将根据父元素的水平或垂直方向计算,而不是内联方向。

例如,您可以强制任何英文元素的文本对齐方式与父元素的对齐方式匹配,而不考虑其书写方向,如下例所示。

div {text-align: start;}
div:lang(en) {direction: ltr;}
div:lang(ar) {direction: rtl;}
p {text-align: match-parent;}

<div lang="en-US">
Here is some en-US text.
<p>The alignment of this paragraph will be to the left, as with its parent.</p>
</div>
<div lang="ar">
هذا نص عربي.
<p>The alignment of this paragraph will be to the right, as with its parent.</p>
</div>

对齐最后一行

有时,您可能希望将元素最后一行的文本与其余内容不同地对齐。例如,使用 text-align: justify 时,最后一行默认为 text-align: start。您可以确保左对齐最后一行,而其他部分则完全两端对齐,或者选择从左对齐切换到居中对齐。对于这些情况,您可以使用 text-align-last

如同 text-align 一样,理解这些值的最快方法是查看 图 15-7。

css5 1507

图 15-7. 不同对齐的最后一行

元素的最后一行根据元素的 text-align-last 值独立对齐,与其他元素无关。

仔细研究 图 15-7 将揭示比块级元素的最后一行更多的影响因素。事实上,text-align-last 适用于任何文本行,这些文本行紧跟在强制换行之前,无论该换行是否由元素的结尾触发。因此,由 <br> 标签创建的换行将使紧随其前的文本行使用 text-align-last 的值。

使用text-align-last时会出现一个有趣的问题:如果元素中的第一行文本也是元素中的最后一行文本,则text-align-last的值优先于text-align的值。因此,以下样式将导致段落居中,而不是起始对齐:

p {text-align: start; text-align-last: center;}
<p>A paragraph.</p>

单词间距

word-spacing 属性用于修改单词之间的间距,接受正或负的长度。这个长度被 添加 到标准单词间的空格中。因此,默认值 normal 等同于设置值 0

如果提供正长度值,单词之间的空间将增加。设置 word-spacing 的负值会使单词更接近:

p.spread {word-spacing: 0.5em;}
p.tight {word-spacing: -0.5em;}
p.default {word-spacing: normal;}
p.zero {word-spacing: 0;}
<p class="spread">The spaces—as in those between the “words”—in this paragraph
   will be increased by 0.5em.</p>
<p class="tight">The spaces—as in those between the “words”—in this paragraph
   will be increased by 0.5em.</p>
<p class="default">The spaces—as in those between the “words”—in this paragraph
   will be neither increased nor decreased.</p>
<p class="zero">The spaces—as in those between the “words”—in this paragraph
   will be neither increased nor decreased.</p>

调整这些设置会产生 图 15-8 中显示的效果。

css5 1508

图 15-8。改变单词间的空间

在 CSS 术语中,单词 是由某种形式的空格包围的任何非空格字符串。这意味着 word-spacing 在任何使用象形文字或非罗马书写风格的语言中都不太可能起作用。这也是为什么前面示例文本中的破折号周围没有空格的原因。从 CSS 的角度来看,“spaces—as” 是一个单词。

使用时要小心。 word-spacing 属性允许创建非常难读的文档,正如 图 15-9 所示。

css5 1509

图 15-9。真正宽的单词间距

字母间距

许多与 word-spacing 相关的问题也会出现在 letter-spacing 中。两者之间唯一的真正区别在于,letter-spacing 修改的是字符或字母之间的间距。

word-spacing 属性一样,letter-spacing 的允许值包括任何长度,尽管建议使用字符相关长度(如 em)而不是根相关长度(如 rem),以确保间距与字体大小成比例。

默认关键字是 normal,其效果与 letter-spacing: 0 相同。输入的任何长度值都会增加或减少字母之间的间距。图 15-10 展示了以下标记的结果:

p {letter-spacing: 0;}    /*  identical to 'normal'  */
p.spacious {letter-spacing: 0.25em;}
p.tight {letter-spacing: −0.25em;}
<p>The letters in this paragraph are spaced as normal.</p>
<p class="spacious">The letters in this paragraph are spread out a bit.</p>
<p class="tight">The letters in this paragraph are a bit smashed together.</p>

css5 1510

图 15-10。不同种类的字母间距
警告

如果页面使用具有连字等特性的字体,并且启用了这些特性,改变字母或单词间距可能会有效地禁用它们。当字母间距被改变时,浏览器不会重新计算连字或其他连接。

间距与对齐

注意单词之间的空间可能会受到text-align属性值的影响。如果一个元素被调整为两端对齐,字母和单词之间的空格可能会被调整以适应行的整体宽度。这可能会进一步影响使用word-spacing声明的间距。

如果将长度值分配给letter-spacing,那么该值不能被text-align改变;但如果letter-spacing的值是normal,则可以通过调整字母间距来使文本两端对齐。CSS 没有规定如何计算间距,因此用户代理使用自己的算法。为了防止text-align改变字母间距,同时保持默认的字母间距,请声明letter-spacing: 0

请注意,计算值是继承的,因此具有较大或较小文本的子元素将具有与其父元素相同的单词或字母间距。您无法定义一个缩放因子,使word-spacingletter-spacing继承到计算值(与line-height相反)。因此,您可能会遇到如图 15-11 所示的问题:

p {letter-spacing: 0.25em; font-size: 20px;}
small {font-size: 50%;}
<p>This spacious paragraph features <small>tiny text that is just
as spacious</small>, even though the author probably wanted the
spacing to be in proportion to the size of the text.</p>

css5 1511

图 15-11. 继承的字母间距

因为inherit继承了祖先的字母间距计算长度,要实现与文本大小成比例的字母间距,唯一的方法是在每个元素上显式设置它,如下所示:

p {letter-spacing: 0.25em;}
small {font-size: 50%; letter-spacing: 0.25em;}

对于单词间距也是一样的。

垂直对齐

现在我们已经讨论了沿内联方向的对齐,让我们继续讨论沿块方向的内联元素的垂直对齐——比如上标和垂直对齐(相对于文本行,如果文本是水平布局的话)。由于行的构造是一个值得一本小书来详细讨论的复杂主题,我们在这里只是简要概述。

调整行高

行之间的距离可以通过改变行高来影响。请注意,在这里“高度”是相对于文本行本身的,假设行的长轴是“宽度”,即使是垂直书写的情况也是如此。从这里我们涵盖的属性名称将显示对西方语言及其书写方向的强烈偏见;这是 CSS 早期的一个副产品,当时只有西方语言可以轻松表示。

line-height 属性是指文本行基线之间的距离,而不是字体的大小,并确定每个元素框的高度增加或减少的量。在最基本的情况下,指定 line-height 是增加(或减少)文本行之间垂直间距的一种方式,但这是看待 line-height 工作方式的一个误导性简单方式。此属性控制行间距,即文本行之间额外的空间,超出字体大小。换句话说,line-height 的值与字体大小的差异即是行间距。

当应用于块级元素时,line-height 定义了该元素内文本基线之间的最小距离。请注意,它定义了一个最小值,而不是一个绝对值。例如,如果一行包含的内联图像或表单控件比声明的行高更高,文本基线可能会被推开超出 line-height 的值。line-height 属性不影响替换元素(如图像)的布局,但仍然适用于它们。

构建一行

正如您在 第六章 中学到的,文本行中的每个元素都生成一个内容区域,其大小由字体大小确定。这个内容区域又生成一个内联框,在没有其他因素的情况下,与内容区域完全相等。由 line-height 生成的行间距是增加或减少每个内联框高度的因素之一。

要确定给定元素的行间距,从 font-size 的计算值中减去 line-height 的计算值。这个值就是总的行间距量。请记住,它可以是负数。然后,将行间距分成两半,每半行间距应用于内容区域的顶部和底部。结果就是该元素的内联框。通过这种方式,只要行的高度没有被替换元素或其他因素强制超出其最小高度,每行文本都会居中于行高之内。

举例来说,假设 font-size(因此内容区域)为 14 像素高,而 line-height 计算为 18 像素。差值(4 像素)被分成两半,每一半应用于内容区域的顶部和底部。这有效地通过创建一个高度为 18 像素的内联框,使内容居中,顶部和底部各增加了 2 像素的空白。这听起来像是描述 line-height 如何工作的一个绕圈子的方式,但是这种描述有其优秀的理由。

一旦给定行内容的所有内联框都已生成,它们将被考虑在行框的构建中。行框的高度正好足以包含最高内联框的顶部和最低内联框的底部。图 15-12 展示了此过程的示意图。

css5 1512

图 15-12. 行框图示

分配 line-height 的值

现在让我们考虑 line-height 的可能值。如果您使用 normal 的默认值,用户代理必须计算行间距。不同的用户代理可能会有不同的值,但 normal 的默认值通常是字体大小的 1.2 倍,这使得行框比给定元素的 font-size 值更高。

许多值是简单的长度度量(例如 18px2em),但在许多情况下,没有长度单位的 <number> 值更可取。

警告

请注意,即使您使用有效的长度测量,例如 4cm,浏览器(或操作系统)可能会使用不正确的度量标准来测量真实世界的测量值,因此线高可能在您的显示器上不会显示为确切的 4 厘米。

emex 和百分比值是相对于元素的 font-size 计算的。以下 CSS 和 HTML 的结果显示在图 15-13 中:

body {line-height: 18px; font-size: 16px;}
p.cl1 {line-height: 1.5em;}
p.cl2 {font-size: 10px; line-height: 150%;}
p.cl3 {line-height: 0.33in;}
<p>This paragraph inherits a 'line-height' of 18px from the body, as well as
a 'font-size' of 16px.</p>
<p class="cl1">This paragraph has a 'line-height' of 24px(16 * 1.5), so
it will have slightly more line-height than usual.</p>
<p class="cl2">This paragraph has a 'line-height' of 15px (10 * 150%), so
it will have slightly more line-height than usual.</p>
<p class="cl3">This paragraph has a 'line-height' of 0.33in, so it will have
slightly more line-height than usual.</p>

css5 1513

图 15-13. 使用 line-height 属性进行简单计算

理解 line-height 和继承

当一个块级元素从另一个元素那里继承了 line-height 时,事情会变得有些棘手。line-height 的值是从父元素计算的,而不是从子元素继承的。以下标记的结果显示在图 15-14 中。这可能不是作者所想要的结果:

body {font-size: 10px;}
div {line-height: 1em;}  /* computes to '10px' */
p {font-size: 18px;}
<div>
<p>This paragraph's 'font-size' is 18px, but the inherited 'line-height'
value is only 10px.  This may cause the lines of text to overlap each
other by a small amount.</p>
</div>

css5 1514

图 15-14. 小 line-height,大 font-size,轻微问题

为什么行之间如此紧凑?因为段落从其父级 <div> 继承了计算的 line-height 值为 10px。解决图 15-14 所示的小 line-height 问题的一个解决方案是为每个元素设置显式的 line-height,但这并不是很实际。一个更好的选择是指定一个数字,实际上设置一个缩放因子:

body {font-size: 10px;}
div {line-height: 1;}
p {font-size: 18px;}

当您指定一个没有长度单位的数字时,会导致缩放因子成为继承值而不是计算值。该数字将应用于元素及其所有子元素,以便每个元素的 line-height 都是相对于其自身的 font-size 计算的(见图 15-15):

div {line-height: 1.5;}
p {font-size: 18px;}
<div>
<p>This paragraph's 'font-size' is 18px, and since the 'line-height'
set for the parent div is 1.5, the 'line-height' for this paragraph
is 27px (18 * 1.5).</p>
</div>

css5 1515

图 15-15. 使用 line-height 因子来克服继承问题

现在您已经基本了解了行如何构建,请让我们讨论相对于行框垂直对齐元素的基本概念——即沿块方向位移它们。

垂直对齐文本

如果您曾经使用过元素 <sup><sub>(上标和下标元素),或者使用了带有图像的废弃的 align 属性,您就已经做了一些基本的垂直对齐。

注意

由于属性名 vertical-align,本节将使用“垂直”和“水平”来指代文本的块级和行内方向。

vertical-align 属性接受八个关键字中的任意一个,百分比值或长度值。这些关键字有些熟悉,有些不熟悉:baseline(默认值)、subsuperbottomtext-bottommiddletoptext-top。我们将研究每个关键字在行内元素中的工作方式。

注意

记住:vertical-align 不会影响块级元素内内容的对齐,只会影响行内内容在文本行或表格单元格中的对齐。这一点可能会在未来发生变化,但截至 2022 年中期,扩展其作用范围的提案尚未推进。

基线对齐

使用 vertical-align: baseline 强制元素的基线与其父元素的基线对齐。大多数情况下,浏览器都会这样做,因为您可能希望一行中所有文本元素的底部对齐。

如果垂直对齐的元素没有基线——即它是图片、表单输入或其他替换元素——那么元素的底部将与其父元素的基线对齐,如图 15-16 所示:

img {vertical-align: baseline;}
<p>The image found in this paragraph <img src="dot.gif" alt="A dot" /> has its
bottom edge aligned with the baseline of the text in the paragraph.</p>

css5 1516

图 15-16. 图像的基线对齐

这种对齐规则很重要,因为它会导致某些网页浏览器始终将替换元素的底边放在基线上,即使行中没有其他文本。例如,假设您在表格单元格中只有一张图片。在某些浏览器中,图片实际上可能处于基线上,但是基线下方的空间会导致图片下方出现间隙。其他浏览器会将图片与表格单元格“包裹”在一起,不会出现间隙。尽管大多数作者不太喜欢间隙的表现,但间隙的行为是正确的。

注意

参见深度老化但仍然相关的文章 “Images, Tables, and Mysterious Gaps”(2002)以获取更详细的间隙行为解释及其解决方法。

上标和下标

声明 vertical-align: sub 会使元素被置于下标位置,意味着其基线(或底部,如果它是替换元素)相对于父元素的基线被降低。规范并未定义元素被降低的距离,因此可能会因用户代理而异。

super 值与 sub 相反;它会使元素的基线(或替换元素的底部)相对于父元素的基线提升。同样,文本被提升的距离取决于用户代理。

请注意,subsuper 这些值会改变元素的字体大小,因此,下标或上标文本不会变小(或变大)。相反,默认情况下,上标或下标元素中的任何文本都与父元素中的文本大小相同,如图 15-17 所示:

span.raise {vertical-align: super;}
span.lower {vertical-align: sub;}
<p>This paragraph contains <span class="raise">superscripted</span>
and <span class="lower">subscripted</span> text.</P>

css5 1517

图 15-17. 上标和下标对齐
注意

如果您希望使上标或下标文本比其父元素的文本小,可以使用 font-size 属性来实现。

顶部和底部对齐

vertical-align: top 选项将元素的行内框顶部与行框顶部对齐。类似地,vertical-align: bottom 将元素的行内框底部与行框底部对齐。因此,以下标记的结果如图 15-18 所示:

.soarer {vertical-align: top;}
.feeder {vertical-align: bottom;}
<p>And in this paragraph, as before, we have
first a <img src="tall.gif" alt="tall" class="soarer" /> image and
then a <img src="short.gif" alt="short" class="soarer" /> image,
and then some text which is not tall.</p>

<p>This paragraph, as you can see, contains
first a <img src="tall.gif" alt="tall" class="feeder" /> image and
then a <img src="short.gif" alt="short" class="feeder" /> image,
and then some text that is not tall.</p>

css5 1518

图 15-18. 顶部和底部对齐

第一段的第二行包含两个内联元素,它们的顶部边缘对齐。它们也远远高于文本基线。第二段显示了相反的情况:两个图像,它们的底部对齐并且远低于其行的基线。这是因为在这两种情况下,行中元素的大小增加了行的高度,超出了字体大小通常创建的高度。

如果您想要将元素与行中文本的顶部或底部对齐,text-toptext-bottom 就是您要寻找的值。对于这些值,替换元素或任何其他类型的非文本元素都会被忽略。而是考虑一个默认文本框。这个默认框源自父元素的 font-size。然后,对齐元素行内框的底部与默认文本框的底部对齐。因此,给定以下标记,您将获得类似于图 15-19 所示的结果:

img.ttop {vertical-align: text-top;}
img.tbot {vertical-align: text-bottom;}
<p>Here: a <img src="tall.gif" class="tbot" alt="tall" /> tall image,
and then a <img src="short.gif" class="tbot" alt="short" /> image.</p>
<p>Here: a <img src="tall.gif" class="ttop" alt="tall"> tall image,
and then a <img src="short.gif" class="ttop" alt="short" /> image.</p>

css5 1519

图 15-19. 文本顶部和底部对齐

中间对齐

middle 值通常(但并非总是)应用于图像。它不会产生您可能期望的确切效果,因为其名称。middle 值将一个内联元素框的中间与父元素的基线上方 0.5ex 的点对齐,其中 1ex 相对于父元素的 font-size 定义。图 15-20 更详细地显示了这一点。

css5 1520

图 15-20. 中间对齐的精确细节

由于大多数用户代理将 1ex 视为半个 em,middle 通常将一个元素的垂直中点与父元素基线上方四分之一 em 的点对齐,尽管这不是一个确定的距离,因此可能因用户代理而异。

百分比

百分比不允许您模拟图像的align="middle"。相反,为vertical-align设置百分比值会根据父元素的基线将元素的基线(或替换元素的底边)提高或降低指定的量。(您指定的百分比是相对于元素的line-height计算的,而不是其父元素。)正百分比值会提升元素,负值则会降低它。

根据文本是如何提升或降低的,它可以看起来被放置在相邻的行中,如图 15-21 所示,因此在使用百分比值时要小心:

sub {vertical-align: −100%;}
sup {vertical-align: 100%;}
<p>We can either <sup>soar to new heights</sup> or, instead,
<sub>sink into despair...</sub></p>

css5 1521

图 15-21. 百分比和有趣的效果

长度对齐

最后,让我们考虑使用特定长度的垂直对齐。vertical-align选项非常基础:它通过声明的距离向上或向下移动元素。因此,vertical-align: 5px;将使元素从未对齐的位置向上移动 5 像素。负长度值会将元素向下移动。

需要意识到的重要一点是,垂直对齐的文本不会成为另一行的一部分,也不会重叠在其他行的文本之上。考虑图 15-22,其中一些垂直对齐的文本出现在段落中间。

css5 1522

图 15-22. 垂直对齐可能导致行高变高

如您所见,任何垂直对齐的元素都会影响行的高度。回想一下行框的描述,它正好与必要的高度相同,以包含最高内联框的顶部和最低内联框的底部。这包括通过垂直对齐向上或向下移动的内联框。

文本转换

在介绍了对齐属性后,让我们看看如何通过属性text-transform来操作文本的大写。

默认值none保留文本并使用源文档中存在的任何大写。正如它们的名称所示,uppercaselowercase将文本转换为全大写或全小写字符。值为full-width会强制在方框内书写字符,就像在排版网格上一样。

警告

可访问性提示:一些屏幕阅读器会逐字母读取所有大写文本,就像拼写首字母缩写一样,即使源文本是小写或混合大小写,并且大写仅通过 CSS 强制。因此,通过 CSS 进行大写文本处理应谨慎对待。

最后,capitalize值仅大写每个单词的第一个字母(其中单词被定义为由空格包围的一系列相邻字符)。图 15-23 以各种方式展示了这些设置:

h1 {text-transform: capitalize;}
strong {text-transform: uppercase;}
p.cummings {text-transform: lowercase;}
p.full {text-transform: full-width;}
p.raw {text-transform: none;}
<h1>The heading-one at the beginninG</h1>
<p>
By default, text is displayed in the capitalization it has in the source
document, but <strong>it is possible to change this</strong> using
the property 'text-transform'.
</p>
<p class="cummings">
For example, one could Create TEXT such as might have been Written by
the late Poet E.E.Cummings.
</p>
<p class="full">
If you need to align characters as if in a grid, as is often done in CJKV
languages, you can use 'full-width' to do so.
</p>
<p class="raw">
If you feel the need to Explicitly Declare the transformation of text
to be 'none', that can be done as well.
</p>

css5 1523

图 15-23. 各种文本转换方式
注意

正如在 第六章 中所指出的,CJK 表示 中文/日文/韩文。CJK 字符占据了整个 Unicode 代码空间的大部分,包括大约 70,000 个汉字。有时您可能会遇到 CJKV 的缩写,它加入了 越南语

不同的用户代理可能有不同的决定单词起始位置的方式,因此哪些字母大写也会不同。例如,在 <h1> 元素中显示的 “heading-one” 文本,在 图 15-23 中,可以以两种方式呈现:“Heading-one” 或 “Heading-One”。CSS 没有规定哪种是正确的,因此两种都可能。

您可能还注意到,在 图 15-23 中 <h1> 元素的最后一个字母仍然是大写的。这是正确的:当应用 text-transform: capitalize 时,CSS 要求用户代理确保每个单词的第一个字母大写,可以忽略其余部分。

作为一种属性,text-transform 可能看起来不重要,但如果您突然决定将所有 <h1> 元素大写,它非常有用。您可以使用 text-transform 来为您进行更改,而不是单独更改所有 <h1> 元素的内容:

h1 {text-transform: uppercase;}
<h1>This is an H1 element</h1>

使用 text-transform 的优点是双重的。首先,您只需编写一个规则即可进行此更改,而不是更改 <h1> 本身。其次,如果以后决定从全大写切换回首字母大写,这种更改甚至更容易:

h1 {text-transform: capitalize;}

请记住,capitalize 仅是在每个“单词”的开头进行的简单字母替换。CSS 不检查语法,因此常见的标题大写约定,如将冠词 (a, an, the) 全部小写,不会被强制执行。

不同的语言有不同的规则决定哪些字母应该大写。text-transform 属性考虑了特定语言的大小写映射。

full-width 选项强制将字符写入一个方块内。您可以在键盘上输入的大多数字符都有正常宽度和全宽度两种形式,具有不同的 Unicode 代码点。当设置和支持 full-width 时,使用全宽度版本可以将其与亚洲表意字符平滑混合,允许表意字符和拉丁脚本对齐。

通常与 <ruby> 注释文本一起使用,full-size-kana 将所有小假名字符转换为等效的全尺寸假名,以弥补通常在 Ruby 中使用的小字体大小的可读性问题。

文本装饰

接下来我们来讨论文本装饰的主题,以及如何使用各种属性影响它们。最简单的文本装饰是下划线,并且可以通过各种属性进行控制。CSS 还支持上划线、穿过线,甚至是您在文字处理程序中看到的波浪下划线,用于标记拼写或语法错误。

我们将从各个单独的属性开始,然后用一个简写属性text-decoration来统一它们。

设置文本装饰线位置

使用属性text-decoration-line,可以设置文本串中一个或多个线条装饰的位置。最熟悉的装饰可能是下划线,多亏了所有的超链接,但 CSS 有三个可能的可见装饰线值(还有一个不支持的第四个,即使它被支持了也不会画出任何线条)。

这些值相对而言具有自解释性:underline在文本下方画一条线,其中under的意思是“在块方向的文本下方”。overline值是其镜像,将线放在文本的块方向上方。line-through值在文本中间画一条线。

让我们看看这些装饰在实践中是什么样子。以下代码在图 15-24 中进行了说明:

p.one {text-decoration: underline;}
p.two {text-decoration: overline;}
p.three {text-decoration: line-through;}
p.four {text-decoration: none;}

css5 1524

图 15-24. 各种文本装饰

值为none会关闭元素上可能已经应用的任何装饰。例如,链接通常默认带有下划线。如果你想要取消超链接的下划线,可以使用以下 CSS 规则来实现:

a {text-decoration: none;}

如果你通过这种规则显式地关闭了链接的下划线,锚点和普通文本之间唯一的视觉区别将是它们的颜色(至少默认情况下,尽管不能保证它们的颜色会有差异)。仅依靠颜色作为区分正常文本和文本内的链接的唯一标识是不够的,这会对用户体验产生负面影响,并使你的内容对许多用户无法访问。

注意

请记住,许多用户会因为你关闭了链接下划线而感到恼火,尤其是在文本块中。如果你的链接没有下划线,用户将很难在文档中找到超链接,对于某些色盲用户来说,这几乎是不可能的。

这就是text-decoration-line的全部内容。你们中的老手可能会认识到这正是text-decoration本身过去所做的事情,但时代在变化,我们在装饰方面可以做的事情远不止放置它们,因此这些值被转移到了text-decoration-line

设置文本装饰颜色

默认情况下,文本装饰的颜色将与文本的颜色相匹配。如果需要更改,text-decoration-color来帮助你。

可以使用任何有效的颜色值作为text-decoration-color,包括关键字currentcolor(这是默认值)。假设你想要清楚地表明删除的文本确实是删除的。那就会像这样:

del, strike, .removed {
	text-decoration-line: line-through;
	text-decoration-color: red;
}

因此,显示的元素不仅将获得删除线装饰,而且线条颜色也将变成红色。除非你也使用color属性改变了文本本身的颜色,否则文本本身不会变成红色。

注意

记住,要保持装饰物与基础文本之间的颜色对比足够高,以保持可访问性。通常单独使用颜色来传达含义是不明智的,比如“检查带有红色下划线的链接获取更多信息!”

设置文本装饰的粗细

使用属性text-decoration-thickness,你可以将文本装饰的线条粗细调整为比通常更粗或可能更细的值。

提供一个长度值将装饰的粗细设置为该长度;因此,text-decoration-thickness: 3px将装饰线条设置为 3 像素厚,无论文本本身有多大或多小。通常更好的方法是使用基于 em 的值或直接使用百分比值,因为百分比是相对于元素的1em值计算的。因此,text-decoration-thickness: 10%在计算的字体大小为 16 像素的字体中将产生 1.6 像素的装饰厚度,但在 40 像素字体大小中将产生 4 像素的装饰厚度。下面的代码展示了一些示例,这些示例在图 15-25 中有详细说明:

h1, p {text-decoration-line: underline;}
.tiny {text-decoration-thickness: 1px;}
.embased {text-decoration-thickness: 0.333em;}
.percent {text-decoration-thickness: 10%;}

css5 1525

图 15-25. 不同装饰的粗细

关键字from-font很有意思,因为它允许浏览器查看字体文件,看看它是否定义了首选的装饰线条粗细;如果有,浏览器将使用该粗细。如果字体文件没有建议的粗细,浏览器则退回到auto行为,并使用它认为合适的粗细,使用的推理只有浏览器自己知道。

设置文本装饰的样式

到目前为止,我们展示了许多简单的单线装饰。如果你渴望超越传统的方法,text-decoration-style提供了一些替代方案。

具体的结果将取决于你选择的值和用于查看结果的浏览器,但这些装饰风格的渲染至少应该与图 15-26 中显示的类似,这是以下代码的输出:

p {text-decoration-line: underline; text-decoration-thickness: 0.1em;}
p.one {text-decoration-style: solid;}
p.two {text-decoration-style: double;}
p.three {text-decoration-style: dotted;}
p.four {text-decoration-style: dashed;}
p.five {text-decoration-style: wavy;}

css5 1526

图 15-26. 各种装饰风格

我们增加了图 15-26 的装饰线条粗细,以提高可读性;默认大小可能使得某些较复杂的装饰风格(如dotted)难以看清。

使用文本装饰的简写属性

当你只想在一个便捷的声明中设置文本装饰的位置、颜色、粗细和样式时,text-decoration就是最佳选择。

使用text-decoration简写属性,你可以将所有内容整合到一起,如下所示:

h2 {text-decoration: overline purple 10%;}
a:any-link {text-decoration: underline currentcolor from-font;}

但要小心:如果将两种不同的装饰匹配到同一个元素,则获胜的规则的值将完全替换输掉的值。考虑以下情况:

h2.stricken {text-decoration: line-through wavy;}
h2 {text-decoration: underline overline double;}

根据这些规则,任何具有stricken类的<h2>元素将只有一条波浪线穿过装饰。双下划线和 overline 装饰将丢失,因为简写值会相互替换而不是累积。

还要注意,由于装饰属性的工作方式,即使有多个装饰,您也只能设置一次颜色和样式。例如,以下内容是有效的,将下划线和 overline 都设置为绿色和虚线:

text-decoration: dotted green underline overline;

如果您希望 overline 与 underline 具有不同的颜色,或者设置每个具有自己的样式,则需要将每个应用于单独的元素,类似于以下示例:

p {text-decoration: dotted green overline;}
p > span:first-child {text-decoration: silver dashed underline;}
<p><span>All this text will have differing text decorations.</span></p>

下划线偏移

除了所有text-decoration属性外,一个相关的属性允许您改变下划线(仅限下划线)与装饰的文本之间的距离:text-underline-offset

您可能希望,例如,超链接上的下划线与文本基线有一定的距离,以便用户更容易注意到。设置像3px这样的长度值将使下划线位于文本基线以下 3 像素处。请参见图 15-27 以查看以下 CSS 的结果:

p {text-decoration-line: underline;}
p.one {text-underline-offset: auto;}
p.two {text-underline-offset: 2px;}
p.three {text-underline-offset: -2px;}
p.four {text-underline-offset: 0.5em;}
p.five {text-underline-offset: 15%;}

css5 1527

图 15-27. 各种下划线偏移

如图 15-27 所示,该值定义了距离文本基线的偏移量,可以是正值(沿着块轴向下)或负值(沿着块轴向上)。

text-decoration-thickness一样,text-underline-offset的百分比值是相对于元素的 1em 计算的。因此,text-underline-offset: 10%会导致在计算的字体大小为 16 像素的字体中产生 1.6 像素的偏移量。

警告

截至 2022 年末,只有 Firefox 支持text-underline-offset的百分比值,这很奇怪,因为百分比值是元素字体中 1 em 的百分比。解决方法是使用 em 长度值,例如 0.1em 代表 10%。

跳墨

过去几节中未解决的一个方面是:浏览器如何精确地在文本上绘制装饰,更准确地说是如何决定何时“跳过”文本的部分?这就是所谓的跳墨,浏览器采用的方法可以通过属性text-decoration-skip-ink进行改变。

当开启跳墨时,装饰会在跨越文本形状的地方中断。通常,这意味着装饰与文本字形之间的小间隙。请参见图 15-28 以查看跳墨方法的差异的近距离插图。

css5 1528

图 15-28. 跳墨方法

这三个值的定义如下:

auto(默认)

浏览器可能会在线条穿过文本字形的地方中断下划线和上划线,线条与字形之间留有一点空间。此外,浏览器应该考虑文本使用的字形,因为某些字形可能会导致墨水跳过,而其他字形则不会。

all

浏览器必须在线条穿过文本字形的地方中断下划线和上划线,并在线条与字形之间留有一点空间。然而,截至 2022 年中期,只有 Firefox 支持这个值。

none

浏览器不得在线条穿过文本字形的地方中断下划线和上划线,而是绘制一条连续的线,尽管可能会在文本字形的某些部分上绘制。

如图 15-28 所示,auto有时可能根据语言、字体或其他因素而有所不同。实际上,你只是告诉浏览器做它认为最好的事情。

注意

尽管此属性的名称以text-decoration-开头,但它text-decoration简写属性所涵盖的属性。这就是为什么在简写之后而不是之前讨论它的原因。

理解奇怪的装饰

现在,让我们来看看text-decoration的不寻常之处。第一个奇怪之处在于text-decoration被继承。没有继承意味着文本上绘制的任何装饰线(无论是下划线、上划线还是穿过文本)的颜色始终相同。即使后代元素是不同的颜色,也是如此,如图 15-29 所示:

p {text-decoration: underline; color: black;}
strong {color: gray;}
<p>This paragraph, which is black and has a black underline, also contains
<strong>strongly emphasized text</strong> that has the black underline
beneath it as well.</p>

css5 1529

图 15-29. 下划线中的颜色一致性

为什么会这样?因为text-decoration的值不会被继承,<strong>元素假设默认值为none。因此,<strong>元素没有下划线。现在,在<strong>元素下面明显有一条线,所以说它没有似乎很愚蠢。尽管如此,它确实没有。你在<strong>元素下看到的是段落的下划线,实际上是“跨越”了<strong>元素。如果你改变粗体元素的样式,就可以更清楚地看到这一点,就像这样:

p {text-decoration: underline; color: black;}
strong {color: gray; text-decoration: none;}
<p>This paragraph, which is black and has a black underline, also contains
<strong>strongly emphasized text</strong> that has the black underline beneath
it as well.</p>

结果与图 15-29 中显示的相同,因为你所做的只是显式声明已经存在的情况。换句话说,没有办法“关闭”由父元素生成的装饰。

有一种方法可以在不违反规范的情况下改变装饰的颜色。正如你记得的那样,在元素上设置文本装饰意味着整个元素具有相同的颜色装饰,即使子元素有不同的颜色。要使装饰颜色与元素匹配,必须显式声明其装饰,如下所示:

p {text-decoration: underline; color: black;}
strong {color: silver; text-decoration: underline;} /*could also use 'inherit'*/
<p>This paragraph, which is black and has a black underline, also contains
<strong>strongly emphasized text</strong> that has the black underline
beneath it as well, but whose gray underline overlays the black underline
of its parent.</p>

在图 15-30 中,<strong> 元素被设置为灰色并具有下划线。灰色下划线在视觉上“覆盖”了父元素的黑色下划线,使装饰的颜色与 <strong> 元素的颜色匹配。黑色下划线仍然存在;灰色下划线只是隐藏它。如果您使用 text-underline-offset 将灰色下划线移动或使父元素的 text-decoration-thickness 宽度超过其子元素,则两条下划线都将可见。

css5 1530

图 15-30. 克服下划线的默认行为

text-decorationvertical-align 结合时,甚至会发生更奇怪的事情。图 15-31 展示了其中一些奇特之处。由于 <sup> 元素本身没有装饰,但它在一个上划线元素内被提升,上划线应该穿过 <sup> 元素的中间:

p {text-decoration: overline; font-size: 12pt;}
sup {vertical-align: 50%; font-size: 12pt;}

css5 1531

图 15-31. 正确,尽管奇怪,装饰行为

但并非所有浏览器都会这样做。截至 2022 年中期,Chrome 会将上划线向上推动,使其横跨在上标的顶部,而其他浏览器则不会。

文本呈现

CSS 的最新补充是 text-rendering,实际上是一个 SVG 属性,在支持的用户代理中被视为 CSS。它允许您指示用户代理在显示文本时应优先考虑什么。

optimizeSpeedoptimizeLegibility 指示应优先考虑绘制速度而不是像调整和连字这样的可读性特征(对于 optimizeSpeed),或者即使这会减慢文本呈现,也应使用这些可读性特征(对于 optimizeLegibility)。

使用 optimizeLegibility 的确切可读性特征未被明确定义,文本呈现通常取决于用户代理运行的操作系统,因此确切的结果可能有所不同。图 15-32 显示了针对速度优化然后针对可读性优化的文本。

css5 1532

图 15-32. 不同的优化

如您在图 15-32 中所见,这两种优化之间的差异在客观上相当小,但它们对可读性可能会产生显著影响。

注意

一些用户代理在优化速度时仍然会始终优化可读性。这可能是过去几年中呈现速度变得如此快的影响。

另一方面,geometricPrecision指示用户代理尽可能精确地绘制文本,以便可以放大或缩小而无损失。您可能认为这总是如此,但并非如此。例如,某些字体在不同的文本大小下会改变字距或连字效果,例如,在较小的大小下提供更多的字距空间,并在增大大小时紧缩字距空间。使用geometricPrecision,这些提示在文本大小变化时被忽略。如果有帮助,可以将其视为用户代理绘制文本,就像所有文本都是一系列 SVG 路径而不是字形。

即使按照通常的网页标准,auto值在 SVG 中的定义也相当模糊:

用户代理应做出适当的权衡来平衡速度、可读性和几何精度,但是可读性比速度和几何精度更重要。

就是这样:用户代理可以做他们认为适合的事情,倾向于可读性。

文本阴影

有时候您确实需要文本投射阴影,比如当文本与多彩背景重叠时。这就是text-shadow的用处所在。语法可能一开始看起来有些古怪,但只需稍加练习就会变得足够清晰。

默认情况下,文本不会有阴影。否则,您可以定义一个或多个阴影。每个阴影由一个可选的颜色和三个长度值定义,最后一个长度值也是可选的。

颜色设置了阴影的颜色,因此可以定义绿色、紫色,甚至白色的阴影。如果省略颜色,则阴影默认为颜色关键字currentcolor,使其与文本本身相同的颜色。

尽管使用currentcolor作为默认颜色可能看起来反直觉,因为您可能认为阴影纯粹是装饰性的,但阴影可以用于提高可读性。一个小的阴影可以使非常细的文本更易读。通过默认使用currentcolor,可以通过阴影增加厚度,该阴影始终与文本的颜色相匹配。

除了通过增加薄文本的粗度来改善可访问性外,阴影还可用于在多色背景上增强颜色对比度。例如,如果您在大部分为黑白的照片上有白色文本,为白色文本添加黑色阴影即使文本覆盖在图像的白色部分上,也能使白色文本的边缘可见。

前两个长度值确定阴影相对于文本的偏移距离;第一个是水平偏移量,第二个是垂直偏移量。要定义一个实心、未模糊的绿色阴影,偏移距离为文本向右 5 像素,向下半个字母高度,如图 15-33 所示,您可以写以下任意一种:

text-shadow: green 5px 0.5em;
text-shadow: 5px 0.5em green;

负长度会使阴影相对于原始文本向左和向上偏移。以下示例,同样显示在图 15-33 中,将一个浅蓝色阴影偏移 5 像素向左和半个字母高度向上:

text-shadow: rgb(128,128,255) −5px −0.5em;

css5 1533

图 15-33. 简单阴影

尽管偏移可能使文本占据更多视觉空间,但阴影对行高没有影响,因此对框模型没有影响。

可选的第三长度值定义了阴影的模糊半径。模糊半径 定义为从阴影轮廓到模糊效果边缘的距离。例如,2 像素的半径将导致模糊填充阴影轮廓和模糊边缘之间的空间。具体的模糊方法没有定义,因此不同的用户代理可能采用不同的效果。例如,以下样式渲染如 图 15-34 所示:

p.cl1 {color: black; text-shadow: gray 2px 2px 4px;}
p.cl2 {color: white; text-shadow: 0 0 4px black;}
p.cl3 {color: black;
       text-shadow: 1em 0.5em 5px red,
	            −0.5em −1em hsla(100,75%,25%,0.33);}

css5 1534

图 15-34. 到处都是阴影
警告

大量的文字阴影或者带有非常大模糊值的文字阴影可能会导致性能下降,特别是在低功率和 CPU 受限的情况下,例如移动设备上的动画。在使用文字阴影的公共设计发布之前,请进行彻底测试。

文本强调

另一种突出文本的方法是为每个字符添加强调标记。这在象形文字语言如汉语或蒙古语中更为常见,但可以通过 CSS 添加到任何语言的文本中。CSS 有三个类似于文本装饰的文本强调属性,还有一个将两者合并的简写。

设置强调样式

三个属性中最重要的一个设置了强调标记的类型,允许您从常见类型列表中选择或提供自定义标记作为文本字符串。

默认情况下,文本没有强调标记,或者是 none。另外,强调标记可以是五种形状之一:dotcircledouble-circletrianglesesame。这些形状可以设置为 filled,这是默认值;或 open,这会将它们呈现为未填充的轮廓。这些内容在 表 15-1 中总结,并在 图 15-35 中显示示例。

表 15-1. 预定义的强调标记

形状 filled open
芝麻 (U+FE45) (U+FE46)
• (U+2022) ◦ (U+25E6)
圆形 ● (U+25CF) ○ (U+25CB)
双圆形 ◉ (U+25C9) ◎ (U+25CE)
三角形 ▲ (U+25B2) △ (U+25B3)

芝麻在竖排书写模式中是最常见的标记;圆形在横排书写模式中通常是默认值。

如果强调标记不能适应当前文本行的高度,则会增加该文本行的高度,直到它们适合而不重叠其他行。与文本装饰和文字阴影不同,文本强调标记确实会影响行高。

如果在你的特定情况下没有预定义的标记适用,你可以提供自己的字符作为字符串(单引号或双引号中的单个字符)。但要小心:如果字符串超过一个字符,浏览器可能会将其缩减为字符串中的第一个字符。因此,text-emphasis-style: 'cool' 可能导致浏览器仅显示 c 作为标记,如图 15-35 所示。此外,在垂直语言的书写方向中,字符串符号可能会旋转或不旋转。

以下是设置强调标记的一些示例:

h1 em {text-emphasis-style: triangle;}
strong a:any-link {text-emphasis-style: filled sesame;}
strong.callout {text-emphasis-style: open double-circle;}

文本强调与文本装饰的一个关键区别是,与装饰不同,强调是继承的。换句话说,如果你在段落上设置了 filled sesame 风格,并且该段落有像链接这样的子元素,那些子元素将继承 filled sesame 的值。

另一个区别是每个字形(字符或其他符号)都有自己的标记,并且这些标记居中于字形上。因此,在像图 15-35 中所见的比例字体中,标记之间的间距会根据相邻的两个字形不同而不同。

css5 1535

图 15-35. 各种强调标记

CSS 规范建议强调标记的大小应为文本字体大小的一半,就像给予了 font-size: 50%。除此之外,它们应该使用与文本相同的文本样式;因此,如果文本是粗体,强调标记也应是粗体。它们还应该使用文本的颜色,除非通过接下来要讨论的属性进行了覆盖。

改变强调颜色

如果你希望强调标记的颜色与其标记的文本不同,text-emphasis-color 就是为你准备的。

默认值通常与颜色相关的属性一样是 currentcolor。这确保了强调标记默认与文本颜色匹配。要进行更改,你可以像下面这样操作:

strong {text-emphasis-style: filled triangle;}
p.one strong {text-emphasis-color: gray;}
p.two strong {text-emphasis-color: hsl(0 0% 50%);}
/* these will yield the same visual result */

放置强调标记

到目前为止,我们展示了特定位置的强调标记:在水平文本中每个字形的上方,以及在垂直文本中每个字形的右侧。这些是默认的 CSS 值,但不一定是首选的位置。text-emphasis-position 属性允许你改变标记的放置位置。

当排版模式为水平时,overunder 值只适用于上述情况。类似地,当排版模式为垂直时,rightleft 值仅适用于右侧和左侧。

这在一些东方语言中可能很重要。例如,中文、日文、韩文和蒙古文在垂直书写时更喜欢将标记放在右侧。它们在水平文本上有所不同:中文更喜欢在文本下方放置标记,而其他语言则更喜欢在文本上方放置标记。因此,你可能会在样式表中写类似以下的内容:

:lang(cn) {text-emphasis-position: under right;}

当文本标记为中文时,这将覆盖默认的over right,应用under right

使用文本强调速记法

text-emphasis属性存在一个速记选项,但仅汇集了样式和颜色。

之所以text-emphasis-position没有包含在text-emphasis速记法中,是因为它可以(实际上必须)单独继承。因此,可以通过text-emphasis更改标记的样式和颜色,而无需在此过程中覆盖位置。

正如前面所述,每个字符、表意字符或其他字形——CSS 称之为排印字符单元——都有其自己的强调标记。这大致是正确的,但也有例外。以下字符单元获得强调标记:

  • 字词分隔符,如空格或任何其他 Unicode 分隔符字符

  • 标点符号,例如逗号、句号和括号

  • 与控制代码对应的 Unicode 符号,或任何未分配的字符

设置文本绘制顺序

浏览器应该按照特定顺序绘制我们之前讨论过的文本装饰、阴影和强调标记,以及文本本身。这些按照从最底部(离用户最远)到最顶部(最靠近用户)的顺序绘制:

  1. 阴影(text-shadow

  2. 下划线(text-decoration

  3. 顶线(text-decoration

  4. 实际文本

  5. 强调标记(text-emphasis

  6. 删除线(text-decoration

因此,文本的阴影被放置在其他所有内容的后面。下划线和顶线在文本后面。强调标记和删除线在文本顶部。请注意,如果您同时拥有顶部文本强调标记和顶线,则强调标记将绘制在顶线之上,重叠部分将遮蔽顶线。

空格

现在我们已经涵盖了各种样式、装饰和其他增强文本的方式,让我们来谈谈white-space属性,它影响用户代理处理文档源中的空格、换行和制表符的方式。

通过使用white-space属性,您可以影响浏览器处理单词和文本行之间空白的方式。在某种程度上,默认的 HTML 处理已经实现了这一点:它将任何空白都折叠为单个空格。因此,给定以下标记,网页浏览器中的呈现将在每个单词之间显示一个空格,并忽略元素中的换行:

<p>This    paragraph   has     many spaces        in it.</p>

您可以使用以下声明显式设置此默认行为:

p {white-space: normal;}

此规则告诉浏览器要像浏览器一直以来所做的那样:丢弃额外的空白。根据这个值,换行字符(回车符)被转换为空格,并且连续超过一个空格的序列被转换为单个空格。

然而,如果将 white-space 设置为 pre,受影响元素中的空白会被视为 HTML <pre> 元素;空白不会被忽略,如图 15-36 所示:

p {white-space: pre;}
<p>This    paragraph   has     many
    spaces        in it.</p>

css5 1536

图 15-36. 在标记中保留空格

使用 white-space 值为 pre,浏览器将注意到额外的空格甚至回车。在这方面,任何元素都可以被制作成 <pre> 元素。

相反的值是 nowrap,它防止文本在元素内部换行,除非使用 <br> 元素。当文本无法换行并且过宽时,会默认出现水平滚动条(可以使用 overflow 属性更改)。以下标记的效果显示在图 15-37 中:

<p style="white-space: nowrap;">This paragraph is not allowed to wrap,
which means that the only way to end a line is to insert a line-break
element.  If no such element is inserted, then the line will go forever,
forcing the user to scroll horizontally to read whatever can't be
initially displayed <br/>in the browser window.</p>

css5 1537

图 15-37. 使用 white-space 属性抑制换行

如果一个元素被设置为 pre-wrap,那么元素内的文本会保留空白序列,但文本行会正常换行。使用此值,生成的换行以及源标记中的换行都会被保留。

pre-line 值与 pre-wrap 相反,导致空白序列像普通文本一样折叠,但保留新行。

break-spaces 值与 pre-wrap 类似,但所有空白都会被保留,即使在行末也会有换行机会。这些空格占据空间,不会悬挂,从而影响框的固有尺寸(最小内容大小和最大内容大小)。

表 15-2 总结了各种 white-space 属性的行为。

表 15-2. white-space 属性

空白 换行 自动换行 结尾空白
pre-line 折叠 保留 允许 移除
normal 折叠 忽略 允许 移除
nowrap 折叠 忽略 阻止 移除
pre 保留 保留 阻止 保留
pre-wrap 保留 保留 允许 悬挂
break-spaces 保留 保留 允许 悬挂

考虑以下标记,其中包含换行(例如,回车)字符来断开行,每行结尾有多个看不见的额外空格字符。结果如图 15-38 所示:

<p style="white-space: pre-wrap;">
This  paragraph      has  a  great   many   s p a c e s   within  its textual
  content,   but their    preservation     will    not    prevent   line
    wrapping or line breaking.
</p>
<p style="white-space: pre-line;">
This  paragraph      has  a  great   many   s p a c e s   within  its textual
  content,   but their collapse  will    not    prevent   line
    wrapping or line breaking.
<p style="white-space: break-spaces;">
This  paragraph      has  a  great   many   s p a c e s   within  its textual
  content,   but their preservation  will    not    prevent   line
    wrapping or line breaking.
</p>

css5 1538

图 15-38. 处理空白的三种方法

请注意第三段落在第一行和第二行之间有一个空行。这是因为源标记中的行尾两个相邻空格之间进行了换行。但在pre-wrappre-line中并未发生,因为这些white-space值不允许空格挂起以创建换行机会。break-spaces值则可以。

空白字符影响多个属性,包括tab-size。当white-space属性设置为不维护空格的值时,它将不会起作用;而overflow-wrap仅在white-space允许换行时才有效。

设置制表位大小

由于在某些white-space值中保留了空格,因此理所当然地制表位(即 Unicode 代码点 0009)将显示为制表符。但每个制表符应等于多少空格?这就是tab-size发挥作用的地方。

默认情况下,当保留空白字符时(如white-space值为prepre-wrapbreak-spaces时),任何制表符字符都将被视为连续八个空格,包括letter-spacingword-spacing的任何效果。您可以通过使用不同的整数值来改变这一点。因此,tab-size: 4将导致每个制表符被呈现为连续四个空格。tab-size不允许负值。

如果提供了长度值,则每个制表符都将使用该长度进行渲染。例如,tab-size: 10px将导致三个制表符序列呈现为 30 像素的空白。tab-size的一些效果在图 15-39 中有所说明。

css5 1539

图 15-39. 不同的制表位长度

请记住,当white-space属性的值导致空格被折叠时,tab-size实际上会被忽略。在这种情况下,计算出来的值仍然存在,但无论源代码中出现多少制表符,都不会有可见效果。

换行和连字符化

处理空白字符当然很好,但更常见的是希望影响可见字符在换行时的处理方式。有几个属性可以影响换行的位置,并支持连字符化。

连字符化

当在移动设备上显示博客文章或《经济学人》部分时,连字符可以非常有用。作者们可以通过使用 Unicode 字符U+00AD 软连字符(或在 HTML 中使用&shy;)来插入自己的连字符提示,但 CSS 也提供了一种在不用提示混乱文档的情况下启用连字符的方法。

使用manual的默认值时,只有在文档中手动插入连字符标记时才插入连字符,例如 U+00AD 或&shy;。否则,不会发生任何连字符。另一方面,none的值则抑制了任何连字符,即使手动换行标记存在;因此,U+00AD 和&shy;会被忽略。

提示

<wbr> 元素不会在换行点引入连字符。若要仅在行末显示连字符,请使用软连字符实体 (&shy;)。

更为有趣(且潜在不一致)的值是 auto,它允许浏览器在单词内的“适当”位置插入连字符和断字,即使没有手动插入的连字符断字也会执行。但是什么构成一个单词?以及在什么情况下插入连字符是合适的?这两者都依赖于语言。用户代理应优先考虑手动插入的连字符断字而不是自动确定的断字,但这并非保证。下面的示例展示了连字符化或抑制连字符化的插图,详见 图 15-40。

.cl01 {hyphens: auto;}
.cl02 {hyphens: manual;}
.cl03 {hyphens: none;}
<p class="cl01">Supercalifragilisticexpialidocious
  antidisestablishmentarianism.</p>
<p class="cl02">Supercalifragilisticexpialidocious
  antidisestablishmentarianism.</p>
<p class="cl02">Super&shy;cali&shy;fragi&shy;listic&shy;expi&shy;ali&shy;
docious anti&shy;dis&shy;establish&shy;ment&shy;arian&shy;ism.</p>
<p class="cl03">Super&shy;cali&shy;fragi&shy;listic&shy;expi&shy;ali&shy;
docious anti&shy;dis&shy;establish&shy;ment&shy;arian&shy;ism.</p>

css5 1540

图 15-40. 连字符化结果

由于连字符化依赖语言,并且 CSS 规范未定义用户代理的确切(甚至模糊)规则,因此在不同浏览器中可能会有所不同。

如果决定进行连字符化,请注意应用连字符化的元素。hyphens 属性是继承的,因此声明 body {hyphens: auto;} 将在文档中的所有内容(包括文本区域、代码示例、块引用等)上应用连字符化。在这些元素级别上阻止自动连字符化可能是个好主意,使用类似以下的规则:

body {hyphens: auto;}
code, var, kbd, samp, tt, dir, listing, plaintext, xmp, abbr, acronym,
blockquote, q, textarea, input, option {hyphens: manual;}

通常情况下,建议在代码示例和代码块中抑制连字符化,特别是在使用连字符作为属性和值名称的语言中。类似的逻辑适用于键盘输入文本,您可能不希望杂乱的破折号出现在 Unix 命令行示例中!如果您决定要对其中一些元素进行连字符化,请将其从选择器中移除。

注意

强烈建议在 HTML 元素上设置 lang 属性,以启用连字符支持并提高可访问性。截至 2022 年中期,hyphens 在 Firefox 支持 30 多种语言,Safari 支持多种欧洲语言,但 Chrome 相关浏览器仅支持英语。

连字符可以通过其他属性的影响来抑制。例如,word-break 影响各种语言中文本软换行的计算方式,决定文本在超出内容框时是否换行。

单词分隔

当一段文本过长无法放入单行时,会进行软换行。这与硬换行相对,后者包括换行字符和 <br> 元素。用户代理决定文本软换行的位置,但 word-break 允许作者影响该决策。

normal的默认值意味着文本应按照其一贯的方式换行。在实际操作中,这意味着文本在单词之间断开,尽管单词的定义因语言而异。在像英语这样的拉丁衍生语言中,这几乎总是在字母序列(例如单词)之间或连字符处断开。在像日语这样的表意语言中,每个符号可以是一个完整的单词,因此断点可以出现在任意两个符号之间。然而,在其他表意语言中,软换行点可能仅限于出现在未空格分隔的符号序列之间。再次强调,默认情况下这是浏览器多年来处理文本的方式。

如果应用break-all值,软换行可以(并且将)出现在任意两个字符之间,即使它们位于单词中间的分隔点上也是如此。使用这个值时,即使软换行发生在连字符处(参见“连字符”),也不会显示连字符。请注意,line-break属性的值(接下来描述)可能会影响在表意文字中的break-all行为。

另一方面,keep-all值抑制字符之间的软换行,即使在每个符号都是一个单词的表意语言中也是如此。因此,在日语中,没有空白的符号序列不会软换行,即使这意味着文本行将超过其元素的长度。(这种行为类似于white-space: pre。)

图 15-41 展示了一些word-break值的示例,而表 15-3 总结了每个值的效果。

css5 1541

图 15-41. 修改断词行为

表 15-3. 断词行为

非 CJK CJK 允许连字符
normal 如常 如常
break-all 在任何字符后 在任何字符后
keep-all 如常 在序列周围

正如前面所述,尽管到 2022 年中期所有已知的浏览器都支持break-word值,但它已被弃用。当使用时,即使overflow-wrap具有不同的值,它也具有相同的效果({word-break: normal; overflow-wrap: anywhere;})。 (我们将在“文本换行”中涵盖overflow-wrap。)

换行处理

如果您对 CJK 文本感兴趣,除了word-break之外,您还需要了解line-break

正如您刚才看到的,word-break可以影响 CJK 文本中文本行的软换行方式。line-break属性也会影响此类软换行,特别是在处理围绕 CJK 特定符号和非 CJK 标点(如感叹号、连字符和省略号)出现在声明为 CJK 文本中的情况时。

换句话说,line-break始终适用于某些 CJK 字符,而不管内容声明的语言是什么。如果你在英文文本段落中加入一些 CJK 字符,line-break仍然会适用于它们,但不适用于文本中的其他任何内容。相反,如果声明内容为 CJK 语言,line-break将继续适用于该 CJK 文本中的这些 CJK 字符,以及CJK 文本中的一些非 CJK 字符。这些包括标点符号、货币符号和其他一些符号。

没有权威的字符受影响和不受影响的列表,但规范提供了一份推荐符号及其周围行为的列表。

默认值auto允许用户代理根据需要自由换行文本,并且更重要的是让用户代理根据情况变化线路断开。例如,用户代理可以对短文本使用较松的换行规则,对长文本使用较严格的规则。实际上,auto允许用户代理在需要时在loosenormalstrict值之间切换,甚至可能在单个元素内逐行切换。

也许你可以推断出其他值有以下一般含义:

loose

这个值施加了“最不限制性”的文本换行规则,适用于行长度较短的情况,例如报纸。

normal

这个值施加了“最常见”的文本换行规则。什么是“最常见”并没有明确定义,尽管有前面提到的推荐行为列表。

strict

这个值施加了“最严格”的文本换行规则。再次强调,这并没有明确定义。

anywhere

这个值在每个排版单元周围创建一个换行机会,包括空格和标点符号。软换行甚至可以发生在一个单词的中间,并且在这种情况下不会应用连字符。

文本换行

在讨论了连字符和软换行的所有信息之后,如果文本仍然溢出其容器会发生什么?这就是overflow-wrap要解决的问题。

最初被称为word-wrapoverflow-wrap属性适用于内联元素,设置浏览器是否应插入换行符以防止文本溢出其行框。与word-break相反,overflow-wrap仅在整个单词无法单独放置在一行上而不溢出时才会创建换行。

这个属性不像它表面看起来的那么直接,因为它的主要效果是改变单词换行和最小内容大小(我们甚至还没有讨论过)在尝试避免文本行末溢出时的交互作用。

注意

只有当white-space的值允许换行时,overflow-wrap属性才能起作用。如果不允许(例如,值为pre),overflow-wrap就没有效果。

如果生效的是默认值normal,则换行会按照正常方式进行——在单词之间或者按语言指示进行。如果单词比包含它的元素的宽度更长,则单词将“溢出”元素框,就像经典的 CSS IS AWESOME 咖啡杯上一样。(如果你之前没见过,去谷歌一下。这值得一笑。)

如果应用了break-word值,单词可以在中间换行,而不会在换行处放置连字符,但这将导致行长与元素的宽度一样。换句话说,如果元素的width属性给定了min-content的值,那么“最小内容”计算将假设内容字符串必须尽可能长。

相比之下,当设置anywhere时,会考虑到换行的机会来进行“最小内容”计算。这意味着,实际上,最小内容宽度将是元素内容中最宽字符的宽度。只有当两个细字符挨在一起时,它们才有机会在同一行上,而在等宽字体中,每行文本都将是一个单独的字符。图 15-42 说明了这三个值之间的区别。

css5 1542

图 15-42. width: min-content的溢出换行

如果width的值不是min-content,那么break-wordanywhere将会有相同的结果。实际上,这两个值唯一的区别在于,对于anywhere,由单词断开引入的软换行机会在计算最小内容内在大小时被考虑。而对于break-word,则不会考虑这些机会。

尽管overflow-wrap: break-word看起来非常类似于word-break: break-all,但它们并不相同。要理解原因,请比较图 15-42 中的第二个框和图 15-41 的顶部中间框。正如显示的那样,只有当内容实际溢出时,overflow-wrap才会生效;因此,当有机会使用源中的空白字符来换行时,overflow-wrap会利用它。相比之下,word-break: break-all会在内容到达换行边缘时引起换行,无论该行中之前是否有任何空白字符。

曾经有一个叫做word-wrap的属性,它与overflow-wrap完全相同。它们如此相似,以至于规范明确指出用户代理“必须将word-wrap视为overflow-wrap属性的替代名称,就好像它是overflow-wrap的简写一样。”

写作模式

之前,我们讨论了内联方向,并介绍了阅读方向的主题。您已经看到了在 HTML 中包含 lang 属性的诸多好处,从能够基于语言选择器进行样式设置,到允许用户代理进行连字。通常情况下,应该让用户代理根据语言属性处理文本的方向,但是 CSS 提供了在必要时进行覆盖的属性。

设置书写模式

用于指定五种可用书写模式之一的属性是 writing-mode。此属性设置元素的块流方向,确定了如何堆叠框。

默认值 horizontal-tb 意味着“水平内联方向和从上到下的块方向”。这涵盖了所有西方和一些中东语言,它们在水平书写方向上可能有所不同。另外两个值提供了竖直内联方向和 RTL 或 LTR 块方向。

sideways-rlsideways-lr 值将水平文本转向“侧面”,文本流动的方向要么从右到左(对于 sideways-rl)要么从左到右(对于 sideways-lr)。与竖排值的区别在于,文本被调整到使其自然阅读的方向。

图 15-43 描述了所有五个值。

css5 1543

图 15-43. 书写模式

注意两个 vertical- 示例中的行是如何串联在一起的。如果你把头向右倾斜,vertical-rl 中的文本至少还能读得通。另一方面,在 vertical-lr 中的文本很难读,因为看起来是从底部向上流动的,至少在排列英文文本时是这样。但在使用 vertical-lr 流动的语言,如某些日语形式,这并不是问题。

在竖排书写模式中,块方向是水平的,这意味着内联元素的竖直对齐会导致它们在水平方向上移动。这在 图 15-44 中有所说明。

css5 1544

图 15-44. 书写模式与“竖直”对齐

所有的上标和下标元素都会导致水平位移,包括它们自身和所占据的行的位置,尽管用于移动它们的属性是 vertical-align。如前所述,垂直位移是相对于行框而言的,其中框的基线被定义为水平的,即使在竖直绘制时也是如此。

感到困惑了吗?没关系。书写模式可能会让你感到困惑,因为它们是一种完全不同的思维方式,并且因为旧版 CSS 规范中的假设与新的能力发生了冲突。如果从一开始就支持了竖排书写模式,vertical-align 可能会有不同的名字——比如 inline-align 或类似的名称。(也许有一天会发生这种情况。)

改变文本方向

一旦确定了写入模式,您可能会决定更改这些文本行中字符的方向。您可能因为各种原因而这样做,其中之一就是使用不同的混合写入系统,如包含英语单词或混合数字的日语文本。在这些情况下,text-orientation 是答案。

text-orientation 属性影响字符的方向。这意味着最佳效果如下样式所示,在 图 15-45 中呈现:

.verts {writing-mode: vertical-lr;}
#one {text-orientation: mixed;}
#two {text-orientation: upright;}
#thr {text-orientation: sideways;}

css5 1545

图 15-45. 文本方向

在 图 15-45 的顶部是一个基本未经样式化的混合日语和英语文本段落。在此之下是三个副本,使用写入模式 vertical-lr。在第一个副本中,text-orientation: mixed 将横向脚本字符(英语)显示为侧向,将竖向脚本字符(日语)显示为直立。在第二个副本中,所有字符都是 upright,包括英语字符。在第三个副本中,所有字符都是 sideways,包括日语字符。

警告

截至 2022 年中期,Chromium 浏览器不支持 sideways

组合字符

只与垂直写入模式相关的 text-combine-upright 属性允许在垂直文本中将一部分字符直立显示。当混合语言或语言片段时,比如在 CJK 文本中嵌入阿拉伯数字时,这可能会很有用,但也可能有其他应用。

本质上,此属性允许您指定字符在垂直文本的情况下是否可以水平排列在一起。您可以选择是否允许所有字符或仅对几个数字字符执行此操作。

工作原理如下:在布局垂直文本行时,浏览器可以考虑两个相邻字符的宽度是否小于或等于文本的 1em 值。如果是,则它们可以相邻放置,有效地将两个字符放置在一个字符的空间内。如果不是,则第一个字符独立放置,此过程继续。

截至 2022 年中期,这可能导致字符非常非常挤压。例如,请考虑以下标记和 CSS:

<div lang="zh-Hant">
<p>这是一些文本</p>
<p class="combine">这是一些文本</p>
<p>这是 117 一些 0 文本 23 日</p>
<p class="combine">这是 117 一些 0 文本 23 日</p>
<p class="combine">
   这是<span>117</span>一些<span>0</span>文本<span>23</span>日</p>
<p>这是<span class="combine">117</span>一些<span
   class="combine">0</span>文本<span class="combine">23</span>日</p>
</div>
p {writing-mode: vertical-rl;}
.combine {text-combine-upright: all;}

所有段落均使用 writing-mode: vertical-rl 编写,但某些段落设置为 text-combine-upright: all,而其他段落则没有。最后一个段落未设置为 all,但其中的 <span> 元素已经设置了。图 15-46 展示了结果。

css5 1546

图 15-46. 各种类型的直立组合

以防你认为这里有 bug,结果在各个浏览器中(截至 2022 年中期)都是一致的。第二和第四列将每个字符,无论是中文汉字还是阿拉伯数字,水平挤压以适应单行。

绕过这个问题的方法是使用子元素分隔文本,就像第五和第六列中所示。在第一个示例中,数字被<span>元素包围,这些元素打破了适合过程。只要没有文本运行有太多字符,超过两到三个符号,文本就会变得越来越难理解。

第六列展示了解决问题的一种方法:将text-combine-upright: all应用于仅有<span>元素的段落中,这些元素已经用于包装阿拉伯数字,通过给每个<span>赋予classcombine。在这种情况下,.combine规则将仅适用于<span>元素,而不是段落中的所有文本。

这就是digits值理论上应该做到的,而无需所有额外的标记。理论上,你可以通过将以下 CSS 应用于没有<span>元素的段落,获得与 Figure 15-46 第六列显示的相同结果:

p {writing-mode: vertical-rl; text-upright-combine: digits 4;}

不幸的是,截至 2022 年中期,除非你计算使用另一个属性名-ms-text-combine-horizontal的 Internet Explorer 11,否则没有浏览器支持此行为。

声明方向

回顾到 CSS2 的时代,一对属性可以通过改变内联基线方向来影响文本的方向:directionunicode-bidi。今天通常不应该使用它们,但是在这里覆盖它们以防你在遗留代码中遇到它们。

警告

CSS 规范明确警告不要在应用于 HTML 文档时使用 CSS 中的directionunicode-bidi。引用一句:“因为 HTML [用户代理]可以关闭 CSS 样式,我们建议…使用 HTML dir属性和<bdo>元素,在没有样式表的情况下确保正确的双向布局。”

属性direction影响块级元素中文本的书写方向,表格列布局的方向,内容在水平溢出其元素框时的方向,以及完全两端对齐元素的最后一行位置。对于内联元素,只有在属性unicode-bidi设置为embedbidi-override时,direction才会应用(请参阅unicode-bidi的描述)。

虽然ltr是默认值,但预期如果浏览器显示 RTL 文本,则该值将被更改为rtl。因此,浏览器可能会携带一个内部规则,类似于以下内容:

*:lang(ar), *:lang(he) {direction: rtl;}

实际规则将更长,并包括所有 RTL 语言,而不仅仅是阿拉伯语和希伯来语,但它说明了问题的要点。

尽管 CSS 试图解决书写方向问题,Unicode 拥有更强大的处理方向性的方法。通过属性unicode-bidi,CSS 作者可以利用 Unicode 的一些能力。

在这里我们将简单地引用 CSS 2.1 规范中的值描述,这些描述很好地捕捉了每个值的本质:

normal

元素不会相对于双向算法打开额外的嵌套级别。对于内联级别元素,隐式重新排序在元素边界之间工作。

embed

如果元素是内联级别的,则此值在双向算法中相对于嵌套打开了一个额外级别。此嵌套级别的方向由direction属性指定。在元素内部,重新排序是隐式完成的。这对应于在元素开头添加“左到右嵌入”字符(U+202A;对于direction: ltr)或“右到左嵌入”字符(U+202B;对于direction: rtl),并在元素末尾添加“弹出方向格式化”字符(U+202C)。

bidi-override

这为内联级别元素创建了一个覆盖。对于块级元素,这为不在另一个块内的内联级别后代创建了一个覆盖。这意味着,在元素内部,重新排序严格按照direction属性的顺序进行;双向算法的隐含部分被忽略。这对应于在元素开头添加“左到右覆盖”字符(U+202D;对于direction: ltr)或“右到左覆盖”字符(U+202E;对于direction: rtl),并在元素末尾添加“弹出方向格式化”字符(U+202C)。

摘要

即使不改变字体,我们也有许多方法来改变文本的外观。除了经典效果如下划线之外,CSS 还允许您在文本上方或穿过文本上绘制线条,更改单词和字母之间的间距,缩进段落的第一行(或其他块级元素),以各种方式对齐文本,对文本的连字和断行施加影响,以及更多。您甚至可以改变文本行之间的间距。CSS 还支持除左到右、上到下书写的语言之外的其他语言。考虑到网页中有如此多的文本,这些属性的强大功能是非常合理的。

第十六章:列表和生成内容

在 CSS 布局领域中,列表是一个有趣的案例。列表中的项目只是块框,但有一个额外的位并不真正参与文档布局,悬挂在一侧。在有序列表中,这个额外的部分包含一系列递增的数字(或字母),由用户代理计算和大部分格式化,而不是作者。从文档结构中获取灵感,用户代理生成数字及其基本呈现方式。

使用 CSS,您可以定义自己的计数模式和格式,并将这些计数器与任何元素关联,而不仅仅是有序列表项。此外,这种基本机制使得可以插入其他类型的内容,包括文本字符串、属性值,甚至外部资源,到文档中。因此,可以使用 CSS 在设计中插入链接图标、编辑符号等,而无需创建额外的标记。

要了解所有这些列表选项如何配合在一起,我们将先探讨基本的列表样式,然后再探讨内容和计数的生成。

与列表一起工作

在某种意义上,几乎任何不是叙述性文本的东西都可以被视为列表。美国人口普查、太阳系、我的家庭树、餐厅菜单,甚至您曾经拥有的所有朋友都可以被表示为列表,或者可能作为列表的列表。这些多种变化使得列表相当重要,这也是为什么遗憾的是 CSS 中的列表样式不够复杂。

影响列表样式的最简单(也是最好支持的)方法是更改其标记类型。列表项的标记是例如出现在无序列表中每个项目旁边的符号。在有序列表中,标记可以是字母、数字或来自某些其他计数系统的符号。您甚至可以用图像替换标记。所有这些都是通过不同的list-style属性实现的。

列表类型

要更改列表项使用的标记类型,请使用list-style-type属性。

您可以使用文本字符串作为标记,例如list-style-type: "▷"。此外,<counter-style>代表可能的关键字列表或使用@counter-style定义的自定义计数器样式(见“定义计数模式”)。这些列表样式类型的几个示例显示在图 16-1 中。

css5 1601

图 16-1. 几种列表样式类型的示例

这些关键字(以及一些特定于浏览器的额外内容)在此列出:

| afaramaric

amaric-abegede

arabic-indic

armenian

asterisks

bengali

binary

cambodian

circle

cjk-decimal *

cjk-earthly-branch

cjk-heavenly-stem

cjk-ideographic

decimal

decimal-leading-zero

devanagari

disc

disclosure-closed

disclosure-open

ethiopic

ethiopic-abegede

ethiopic-abegede-am-et

ethiopic-abegede-gez

ethiopic-abegede-ti-er

ethiopic-abegede-ti-et

埃塞俄比亚语-哈勒哈米 ‡, -

埃塞俄比亚语-哈勒哈米-阿姆哈拉-厄

埃塞俄比亚语-哈勒哈米-阿姆哈拉

埃塞俄比亚语-哈勒哈米-阿姆哈拉 -

埃塞俄比亚语-哈勒哈米-阿姆哈拉

埃塞俄比亚语-哈勒哈米-盖兹

埃塞俄比亚语-哈勒哈米-奥姆哈拉

| 埃塞俄比亚语-哈勒哈米-西达马埃塞俄比亚语-哈勒哈米-索马利

埃塞俄比亚语-哈勒哈米-提-厄 -

埃塞俄比亚语-哈勒哈米-提格

埃塞俄比亚语-哈勒哈米-提格

埃塞俄比亚-数字符号

脚注

格鲁吉亚

古吉拉特语

古尔穆克希

朝鲜文 -

朝鲜文元音 -

希伯来语

平假名

平假名-伊吕波

日语(正式)

日语(非正式)

卡纳达语

片假名

katakana-iroha

高棉语

朝鲜语-汉字(正式)

朝鲜语-汉字(正式)

朝鲜语-汉字(非正式)

老挝语

lower-alpha

小写亚美尼亚字母

小写希腊字母

小写十六进制

小写拉丁字母

小写挪威文

小写罗马字母

马拉雅拉姆

| 蒙古文 缅甸文

八进制

奥里亚

奥罗莫

波斯语

希达马

简体中文(正式)

简体中文(非正式)

索马里

方形

符号 *

泰米尔语 *

泰卢固语

泰语

藏文

提格雷

提格利尼亚语-厄

提格利尼亚-厄利特阿贝吉德

提格利尼亚语-厄利特

提格利尼亚语-厄利特阿贝吉德

繁体中文(正式)

繁体中文(非正式)

upper-alpha

upper-armenian

大写希腊字母

大写十六进制

大写拉丁字母

大写挪威文

大写罗马字母

乌尔都语 -

|

† 仅限 WebKit
‡ 除了 WebKit 之外的所有引擎 *
* 仅限 Mozilla
- 在 Firefox 中需要 -moz- 前缀

如果您提供浏览器不识别的计数样式,例如声明 list-style-type: lower-hexadecimal 并加载页面,则一些浏览器(包括 Firefox、Edge 和 Chrome)将假定为 decimal。Safari 将忽略它不理解的值作为无效。

list-style-type 属性,以及所有其他与列表相关的属性,只能应用于具有 display: list-item 的元素,但 CSS 不区分有序和无序列表项。因此,您可以设置有序列表使用圆点而不是数字。实际上,list-style-type 的默认值是 disc,所以您可能会推断,没有明确声明的情况下,所有列表(有序或无序)都将使用圆点作为每个项目的标记。这是合乎逻辑的,但事实证明,这取决于用户代理。即使用户代理没有预定义的规则,如 ol {list-style-type: decimal;},它可能会禁止有序标记应用于无序列表,反之亦然。您不能依赖于此,请小心。

如果您想完全隐藏标记的显示,应该使用 none。这个值会导致用户代理在标记通常出现的地方不放置任何东西,尽管它不会中断有序列表的计数。因此,以下标记会产生 图 16-2 中显示的结果:

ol li {list-style-type: decimal;}
li.off {list-style-type: none;}
<ol>
<li>Item the first
<li class="off">Item the second
<li>Item the third
<li class="off">Item the fourth
<li>Item the fifth
</ol>

css5 1602

图 16-2. 关闭列表项标记

list-style-type属性是继承的,因此如果你想在嵌套列表中使用不同样式的标记,你可能需要单独定义它们。你可能还需要显式地为嵌套列表声明样式,因为用户代理的样式表可能已经定义了这些。例如,假设用户代理已定义以下样式:

ul {list-style-type: disc;}
ul ul {list-style-type: circle;}
ul ul ul {list-style-type: square;}

如果情况确实如此——这种情况或类似情况很可能会出现——你将不得不声明自己的样式以覆盖用户代理的样式。继承将不足以应对。

字符标记

CSS 还允许作者提供字符串值作为列表标记。这打开了从键盘输入的任何内容的可能性,只要你不介意在列表中的每个标记使用相同的字符串。图 16-3 展示了以下样式的结果:

.list01 {list-style-type: "%";}
.list02 {list-style-type: "Hi! ";}
.list03 {list-style-type: "†";}
.list04 {list-style-type: "⌘";}
.list05 {list-style-type: "";}

css5 1603

图 16-3。字符串标记的示例

列表项图像

有时普通文本标记可能不够用。你可能更喜欢为每个标记使用图像,这可以通过list-style-image属性实现。

它是如何工作的:

ul li {list-style-image: url(ohio.gif);}

是的,就是这么简单。一个简单的url()值,你就可以将图像用作标记,正如你在图 16-4 中所看到的。

css5 1604

图 16-4。使用图像作为标记

列表图像标记以其完整尺寸显示,因此在选择图像时要小心,正如图 16-5 所示的示例清楚地表明了其超大尺寸的标记:

ul li {list-style-image: url(big-ohio.gif);}

css5 1605

图 16-5。使用超大图像作为标记

通常最好提供一个备用的标记类型,以防你的图像不加载、损坏或者是一些用户代理不能显示的格式。通过为列表定义一个备用的list-style-type来实现这一点:

ul li {list-style-image: url(ohio.png); list-style-type: square;}

你还可以通过将list-style-image设置为默认值none来使用它。这是一个好的做法,因为list-style-image是继承的,所以任何嵌套列表将会继承该图像作为标记,除非你阻止这种情况发生:

ul {list-style-image: url(ohio.gif); list-style-type: square;}
ul ul {list-style-image: none;}

由于嵌套列表继承了项目类型为square,但已设置为不使用图像作为其标记,因此在嵌套列表中使用方块作为标记,正如图 16-6 中所示。

css5 1606

图 16-6。关闭子列表中的图像标记

list-style-image允许任何图像值,包括渐变图像。因此,以下样式将产生类似于图 16-7 所示的结果:

.list01 {list-style-image:
    radial-gradient(closest-side,
        orange, orange 60%, blue 60%, blue 95%, transparent);}
.list02 {list-style-image:
    linear-gradient(45deg, red, red 50%, orange 50%, orange);}
.list03 {list-style-image:
    repeating-linear-gradient(-45deg, red, red 1px, yellow 1px, yellow 3px);}
.list04 {list-style-image:
    radial-gradient(farthest-side at bottom right,
        lightblue, lightblue 50%, violet, indigo, blue, green,
        yellow, orange, red, lightblue);}

css5 1607

图 16-7。渐变列表标记

渐变标记有一个缺点:它们往往非常小。这可能受到诸如字体大小之类的因素的影响,因为标记的大小往往随列表项内容的缩放而变化。如果你需要完全控制标记的渲染方式,不要使用::marker;而是使用::before

注意

直接样式化列表标记的方法是使用伪元素::marker,本章稍后将详细讨论。

列表标记位置

还有另一件事可以改变列表项的外观:决定标记是出现在列表项内容的外部还是内部。这可以通过list-style-position来实现。

如果标记的位置设置为outside(默认值),它将以网页开始时列表项的方式显示。如果您希望外观略有不同,可以通过将list-style-position的值设置为inside来将标记拉向内容。这将导致标记位于列表项内容的“内部”。确切的方式未定义,但图 16-8 展示了一种可能性:

li.first {list-style-position: inside;}
li.second {list-style-position: outside;}

css5 1608

图 16-8. 将标记放置在列表项内部和外部

在实践中,给定inside位置的标记被视为插入到列表项内容开头的内联元素。这并不意味着标记内联元素。除非您将所有其他内容包装在像<span>这样的元素中,或者直接(但对允许的属性有严格限制)通过使用::marker直接处理它们,否则您无法与元素的其他内容单独样式化它们。只是在布局术语上,它们表现得像这样。

缩写的列表样式

为了简洁起见,您可以将这三个list-style属性合并为一个便捷的单一属性:list-style

例如:

li {list-style: url(ohio.gif) square inside;}

如图 16-9 所示,所有三个值可以同时应用于列表项。

css5 1609

图 16-9. 将一切汇总

list-style的值可以按任意顺序列出,也可以省略任何一个。只要有一个存在,其余的将填充它们的默认值。例如,以下两条规则将有相同的视觉效果:

li.norm {list-style: url(img42.gif);}
li.odd {list-style: url(img42.gif) disc outside;} /* the same thing */

它们还将覆盖任何先前的规则。例如:

li {list-style-type: square;}
li {list-style: url(img42.gif);}
li {list-style: url(img42.gif) disc outside;} /* the same thing */

结果将与图 16-9 中的结果相同,因为隐含的list-style-typedisc将覆盖先前声明的square值,就像显式值disc在第二条规则中覆盖它一样。

列表布局

现在我们已经了解了样式化列表标记的基础知识,让我们来考虑在各种浏览器中如何布局列表。我们将从一组三个没有任何标记且尚未放置在列表中的列表项开始,如图 16-10 所示。

css5 1610

图 16-10. 三个列表项

列表项周围的边框显示它们本质上类似于块级元素。事实上,值list-item被定义为生成块级盒子。现在让我们添加标记,如图 16-11 所示。

css5 1611

图 16-11. 添加了标记

标记与列表项内容之间的距离未由 CSS 定义,而 CSS 目前还没有提供直接影响该距离的方法。

当标记位于列表项内容外部时,它们不会影响其他元素的布局,甚至不会真正影响列表项本身的布局。它们只是距离内容边缘一定距离悬挂,无论内容边缘去哪里,标记都会跟随。标记的行为很像标记在相对于列表项内容绝对定位的情况下,类似于position: absolute; left: -1.5em;。当标记在内部时,它就像一个内联元素位于内容开头。

到目前为止,我们尚未添加实际的列表容器;图示中既没有<ul>也没有<ol>元素。我们可以将它们加入到混合中,如图 16-12 所示(用虚线边框表示)。

css5 1612

图 16-12. 添加列表边框

与列表项一样,无序列表元素生成一个块级框,包含其后代元素。如图 16-12 所示,标记不仅放置在列表项内容外部,也放置在无序列表元素的内容区域外。您期望从列表中看到的常规“缩进”尚未指定。

大多数浏览器,在本文写作时,通过设置包含列表元素的填充或边距来缩进列表项。例如,用户代理可能会应用如下规则:

ul, ol {margin-inline-start: 40px;}

大多数浏览器使用类似以下规则:

ul, ol {padding-inline-start: 40px;}

两种方法都没有错,但由于浏览器可以且已经改变了它们缩进列表内容的方式,我们建议在试图消除列表项缩进时同时包含这两个属性的值。图 16-13 比较了这两种方法。

css5 1613

图 16-13. 边距和填充作为缩进设备
提示

距离40px是早期 Web 浏览器的遗留物,它们通过像素量来缩进列表(块引用也是以相同距离缩进的)。一个很好的替代值可能是2.5em,它会随文本大小的变化而缩放,并且在默认字体大小为 16 像素时也等于40px

对于希望更改列表缩进距离的作者,我们强烈建议同时指定填充和边距,以确保跨浏览器的兼容性。例如,如果您想使用填充来缩进列表,请使用以下规则:

ul {margin-inline-start: 0; padding-inline-start: 1em;}

如果您更喜欢使用边距,可以这样写:

ul {margin-inline-start: 1em; padding-inline-start: 0;}

无论哪种情况,记住标记将相对于列表项的内容放置,因此可能“悬挂”在文档的主文本之外,甚至超出浏览器窗口的边缘。如果使用非常大的图像或长文本字符串作为列表标记,这最容易观察,如在图 16-14 中所示。

css5 1614

图 16-14. 大标记和列表布局

::marker伪元素

许多作者要求的一个功能是能够控制标记和列表项内容之间的空间,或者独立于列表项内容更改列表标记的大小或颜色。

列表标记可以在一定程度上用伪元素::marker进行样式设置。截至 2022 年底,::marker规则允许的属性如下:

  • content

  • color

  • text-combine-upright

  • unicode-bidi

  • direction

  • white-space

  • 所有的font-*属性

  • 所有的过渡和动画属性

您可能已经注意到,没有包括任何元素大小或其他盒模型属性,例如边距,这对于许多作者希望的标记样式构成了阻碍。未来可能会添加更多属性,但目前只有这些。

几个标记样式的示例,如在图 16-15 中声明的:

li:nth-child(1)::marker {color: gray;}
li:nth-child(2)::marker {font-size: 2em;}
li:nth-child(3)::marker {font-style: italic;}
<ol>
	<li>List item the first</li>
	<li>The second list item</li>
	<li>List Items With a Vengeance</li>
</ol>

<ul>
	<li>List item the first</li>
	<li>The second list item</li>
	<li>List Items With a Vengeance</li>
</ul>

css5 1615

图 16-15. 标记样式示例

请注意,对于本列表的有序和无序版本,倍增标记字体大小的结果不同。这归结于两种标记类型的不同默认大小和放置方式。正如前面所述,您对标记的控制程度有限,即使使用content定义了标记。因此,当您绝对需要完全创意自由时,通常最好使用生成内容或标记的内联内容来构建自己的标记。

创建生成的内容

CSS 定义了创建生成内容的方法。这是通过 CSS 插入的内容,但不由标记或内容表示。

例如,列表标记是生成的内容。列表项的标记没有直接在标记中表示,作者无需将标记写入文档内容中。浏览器会自动生成适当的标记。对于无序列表,标记将具有某种符号,如圆圈、圆盘或方形。对于有序列表,默认情况下,标记是一个按顺序递增的计数器,每个连续的列表项递增一次。(或者,正如您在前几节中看到的那样,您可以用图像或符号替换任一种类—正如您稍后将看到的,任何由content属性支持的内容。)

要了解如何影响列表标记并自定义有序列表的计数(或其他任何内容!),您必须首先查看更基本的生成内容。

插入生成的内容

要将生成的内容插入文档中,请使用 ::before::after 伪元素。通过 content 属性(在下一节中描述),这些伪元素可以在元素的内容之前或之后放置生成的内容。

例如,你可能希望在每个超链接前面加上文本“(link)”以便在打印页面时标记它们。可以通过媒体查询和以下规则来实现这一效果,如 图 16-16 所示:

@media print{
	a[href]::before {content: "(link)";}
}

css5 1616

图 16-16. 生成文本内容

注意生成的内容和元素内容之间没有空格。这是因为前面例子中的 content 的值不包括空格。可以修改声明如下以确保生成和实际内容之间有空格:

a[href]::before {content: "(link) ";}

这是一个小差异,但是一个重要的差异。

类似地,你可能选择在 PDF 文档链接的末尾插入一个小图标。实现这一效果的规则会类似于这样:

a.pdf-doc::after {content: url(pdf-doc-icon.gif);}

假设你想通过给这些链接周围加上边框来进一步样式化它们。这可以通过第二条规则来完成,如 图 16-17 所示:

a.pdf-doc {border: 1px solid gray;}
<<generated-content-icons>> shows the result of these two rules.

css5 1617

图 16-17. 生成图标

注意链接边框围绕生成的内容扩展,就像链接下划线围绕 图 16-16 中的“(link)”文本一样。这是因为默认情况下,生成的内容被放置在元素的元素框内(除非生成的内容是列表标记)。

你可以浮动或定位生成的内容到其父元素框之外。可以给生成的内容的所有 display 值,可以将块格式应用于内联框的生成内容,反之亦然。例如,考虑这个例子:

em::after {content: " (!) "; display: block;}

即使 em 是内联元素,生成的内容也会生成一个块级盒子。同样地,给定以下代码,生成的内容将变成块级而不是保持默认的 inline

h1::before {content: "New Section"; display: block; color: gray;}

图 16-18 展示了结果。

css5 1618

图 16-18. 生成块级内容

生成内容的一个有趣的方面是它继承来自其附加到的元素的值。因此,给定以下规则,生成的文本将是绿色的,与段落内容相同:

p {color: green;}
p::before {content: "::: ";}

如果你想生成的文本是紫色的,只需一个简单的声明即可:

p::before {content: "::: "; color: purple;}

当然,这种值的继承只发生在继承属性中。这点值得注意,因为它影响了处理某些效果的方式。考虑以下情况:

h1 {border-top: 3px solid black; padding-top: 0.25em;}
h1::before {content: "New Section"; display: block; color: gray;
  border-bottom: 1px dotted black; margin-bottom: 0.5em;}

由于生成的内容被放置在 <h1> 元素的元素框内,它将被放置在元素顶部边框下方。它还会被放置在任何填充内,如 图 16-19 所示。

css5 1619

图 16-19. 考虑放置位置

已被设置为块级的生成内容的底部边距将通过半个 em 向下推动元素的实际内容。在这个例子中,生成的内容的效果是将<h1>元素分成两部分:生成内容框和实际内容框。这是因为生成的内容具有display: block。如果您将其更改为display: inline(或完全删除display:block;),效果将如图 16-20 所示:

h1 {border-top: 3px solid black; padding-top: 0.25em;}
h1::before {content: "New Section"; display: inline; color: gray;
  border-bottom: 1px dotted black; margin-bottom: 0.5em;}

css5 1620

图 16-20. 将生成内容更改为内联

注意边框的放置方式以及顶部填充仍然受到尊重。所生成内容的底部边距也受到尊重,但由于生成的内容现在是内联的,边距不影响行高,因此边距没有可见效果。

基本生成内容的介绍完成后,让我们更仔细地看一下如何指定实际生成的内容。

指定内容

如果要生成内容,您需要一种描述它的方法。正如您已经看到的,这是通过content属性处理的,但是这个属性比您迄今所见到的要复杂得多。

您已经在操作中看到了字符串和 URI 值,本章稍后会涵盖计数器。在我们查看attr()和引号值之前,让我们稍微详细讨论一下字符串和 URI。

字符串值是按照字面方式呈现的,即使它们包含本质上应该是某种标记的内容。因此,下面的规则将被直接插入到文档中,如图 16-21 所示:

h2::before {content: "<em>&para;</em> "; color: gray;}

css5 1621

图 16-21. 字符串会原样显示

这意味着,如果您希望换行(回车)作为生成内容的一部分,您不能使用<br>。而是使用字符串\A\00000a,这是表示换行的 CSS 方式(基于 Unicode 换行字符,其十六进制位置为A)。相反,如果您有一个长字符串值并需要将其分成多行,则使用\字符转义换行符。以下规则示例演示了这两者,并在图 16-22 中进行了说明:

h2::before {content: "We insert this text before all H2 elements because \
it is a good idea to show how these things work. It may be a bit long \
but the point should be clearly made.  "; color: gray;}

css5 1622

图 16-22. 插入和抑制换行符

您还可以使用转义来引用十六进制 Unicode 值,例如\00AB

警告

到目前为止,在插入转义内容(如\279c)方面得到了很好的支持,但是某些浏览器不支持转义换行符\A\0000a,且没有浏览器支持\A,除非在其后添加空格。

使用 URI 值,可以指向外部资源(例如图像、电影、声音剪辑或用户代理支持的任何其他内容),然后将其插入到文档的适当位置。如果用户代理因任何原因无法支持您指向的资源(例如,尝试在打印文档时插入电影),则用户代理必须完全忽略该资源,不会插入任何内容。

插入属性值

有时您可能希望获取元素属性的值并使其成为文档显示的一部分。举个简单的例子,您可以将每个链接的 href 属性的值立即放在链接后面,如下所示:

a[href]::after {content: attr(href);}

此操作可能导致生成的内容直接与实际内容相撞。为解决此问题,可以在声明中添加一些字符串值,其结果如 图 16-23 所示:

a[href]::after {content: " [" attr(href) "]";}

css5 1623

图 16-23. 插入 URL

例如,这对打印样式表非常有用。可以插入任何属性值作为生成的内容:alt 文本、classid 值等。作者可以选择使引用信息对块引用明确化,如下所示:

blockquote::after {content: "(" attr(cite) ")"; display: block;
  text-align: right; font-style: italic;}

对于其他复杂的规则,可能会显示传统文档的文本和链接颜色值:

body::before {
  content: "Text: " attr(text) " | Link: " attr(link)
  " | Visited: " attr(vlink) " | Active: " attr(alink);
  display: block; padding: 0.33em;
  border: 1px solid; text-align: center; color: red;}

请注意,如果属性不存在,则会放置一个空字符串。这在 图 16-24 中有所体现,该示例应用于一个 body 元素没有 alink 属性的文档。

css5 1624

图 16-24. 跳过缺失的属性

文本 “Active: ”(包括尾随空格)被插入到文档中,正如您所见,但后面没有内容跟随。当您希望仅在存在属性时插入属性值时,这非常方便。

警告

CSS 定义属性引用的返回值为未解析字符串。因此,如果属性值包含标记或字符实体,它们将按原样显示。

使用生成的引号

生成内容的一个专门形式是引号,CSS 提供了一种强大的方式来管理引号及其嵌套行为。这是可能的,因为可以像 open-quotequotes 属性一样对内容值进行配对。

除了关键字 noneinherit 外,唯一有效的值是一个或多个字符串对,每对中的第一个值是 open-quote 的值,第二个值是 close-quote 的值。因此,在以下两个声明中,只有第一个是有效的:

quotes: '"' "'";  /* valid */
quotes: '"';  /* NOT VALID */

第一个规则还说明了在字符串周围放置字符串引号的一种方法。双引号由单引号包围,反之亦然。

让我们看一个简单的例子。假设您正在创建一个用于存储喜爱引用的 XML 格式。以下是列表中的一个条目:

<quotation>
  <quote>I hate quotations.</quote>
  <quotee>Ralph Waldo Emerson</quotee>
</quotation>

为了以有用的方式呈现数据,您可以使用以下规则,并显示结果如 图 16-25 所示:

quotation {display: block;}
quote {quotes: '“' '”';}
quote::before {content: open-quote;}
quote::after {content: close-quote;}
quotee::before {content: " (";}
quotee::after {content: ")";}

css5 1625

图 16-25. 插入引号和其他内容

open-quoteclose-quote用于插入适当的引号符号(因为不同语言有不同的引号)。它们使用引用的值来确定它们应该如何工作。因此,引号以双引号开始和结束。

使用引号可以定义引号模式,可以嵌套到任意深度。例如,在美国英语中,一个常见的做法是从双引号开始,然后在第一个引号内部使用单引号。可以通过以下规则使用花括号引号重现此操作:

quotation: display: block;}
quote {quotes: '\201C' '\201D' '\2018' '\2019';}
quote::before, q::before{content: open-quote;}
quote::after, q::after {content: close-quote;}

当应用于以下 XML 时,这些规则将产生 图 16-26 所示的效果:

<quotation>
 <quote> In the beginning, there was nothing. And God said: <q>Let there
  be light!</q> And there was still nothing, but you could see it.</quote>
</quotation>

css5 1626

图 16-26. 嵌套的花括号引号

如果引号的嵌套级别大于定义的配对数,则深层级别将重复使用最后一个配对。因此,如果我们将以下规则应用于 图 16-26 中显示的标记,则内部引号将使用双引号,与外部引号相同:

quote {quotes: '\201C' '\201D';}
提示

这些特定规则使用花括号引号符号的十六进制 Unicode 位置。如果您的 CSS 使用 UTF-8 字符编码(实际上应该如此),则可以跳过转义的十六进制位置方法,直接包含花括号引号字符,就像前面的示例一样。

生成的引号使得另一种常见的排版效果成为可能。当引用文本跨越多个段落时,通常省略每个段落的close-quote;仅显示开头的引号标记,除了最后一个段落。可以使用no-close-quote值重现此效果:

blockquote {quotes: '"' '"' "'" "'" '"' '"';}
blockquote p::before {content: open-quote;}
blockquote p::after {content: no-close-quote;}
blockquote p:last-of-type::after {content: close-quote;}

这将使每个段落以双引号开头,但没有结束标记。对于最后一个段落也是如此,因此前一个代码块的第四行在最后一个段落的末尾插入了闭合引号。

此值很重要,因为它减少了引号的嵌套级别而不生成符号。这就是为什么每个段落都以双引号开头,而不是在第三段落之前交替使用双引号和单引号。no-close-quote值在每个段落结束时关闭引用嵌套,因此每个段落都以相同的嵌套级别开头。

这很重要,因为 CSS2.1 规范指出:“引用深度与源文档的嵌套或格式结构无关。”换句话说,当您开始一个引用级别时,它在元素之间保持,直到遇到close-quote,引用嵌套级别将递减。

为了完整起见,还有一个no-open-quote关键字,其对称地与no-close-quote具有相同的效果。此关键字将引用嵌套级别增加一级,但不生成符号。

定义计数器

即使您没有意识到,您可能也熟悉计数器;例如,有序列表中的列表项标记即为计数器。两个属性和两个content值几乎可以定义任何计数格式,包括使用多种样式的子节计数器,例如“VII.2.c”。

重置和递增

我们通过设置计数器的起始点然后按指定数量增加来创建计数器。前者由属性counter-reset处理。

计数器标识符只是作者创建的标签。例如,您可能会将子节计数器命名为subsectionsubsecssbob。仅仅通过重置(或增加)一个标识符就足以将其调用出来。在以下规则中,计数器chapter被定义为其被重置的方式:

h1 {counter-reset: chapter;}

默认情况下,计数器被重置为 0。如果您想重置为不同的数字,可以在标识符后声明该数字:

h1#ch4 {counter-reset: chapter 4;}

您还可以通过列出空格分隔的标识符-整数对一次性重置多个标识符。如果省略整数,则默认为 0:

h1 {counter-reset: chapter 4 section -1 subsec figure 1;}
   /* 'subsec' is reset to 0 */

如前例所示,可以使用负值。将计数器设置为-32768并从那里开始计数是完全合法的。

警告

CSS 未定义用户代理在非数字计数样式中处理负计数值的行为。例如,如果计数器的值为-5,但其显示样式为upper-alpha,则没有定义的行为。

要进行计数,您需要一个属性来指示元素增加或减少计数器。否则,计数器将保持counter-reset声明给定的任何值。相关属性是counter-increment

counter-reset类似,counter-increment接受标识符-整数对,这些对中的整数部分可以是 0 或负数,也可以是正数。区别在于,如果从对中省略整数,它默认为 1,而不是 0。

作为示例,这是用户代理如何定义计数器以重新创建有序列表的传统计数方式 1、2、3 的方式:

ol {counter-reset: ordered;}  /* defaults to 0 */
ol li {counter-increment: ordered;}  /* defaults to 1 */

另一方面,作者可能希望从 0 开始倒数,以便列表项使用上升负数系统。这只需要进行小修改:

ol {counter-reset: ordered;}  /* defaults to 0 */
ol li {counter-increment: ordered -1;}

列表的计数将会是 -1、-2、-3 等。如果用整数 -1 替换 -2,列表将会是 -2、-4、-6 等。

显示计数器

要显示计数器,你需要在content属性中与计数器相关的值结合使用。为了演示其工作原理,让我们使用基于 XML 的有序列表:

<list type="ordered">
 <item>First item</item>
 <item>Item two</item>
 <item>The third item</item>
</list>

通过将以下规则应用于使用此结构的 XML,你将获得图 16-27 中显示的结果:

list[type="ordered"] {counter-reset: ordered;}  /* defaults to 0 */
list[type="ordered"] item {display: block;}
list[type="ordered"] item::before {counter-increment: ordered;
     content: counter(ordered) ". "; margin: 0.25em 0;}

css5 1627

图 16-27. 项目计数

生成的内容被放置在关联元素的内联内容之前。因此,效果类似于声明了list-style-position: inside;的 HTML 列表。

<item> 元素是生成块级框的普通元素,这意味着计数器不仅限于具有display: list-item的元素。事实上,任何元素都可以使用计数器。考虑以下规则:

h1 {counter-reset: section subsec;
    counter-increment: chapter;}
h1::before {content: counter(chapter) ". ";}
h2 {counter-reset: subsec;
    counter-increment: section;}
h2::before {content: counter(chapter )"." counter(section) ". ";}
h3 {counter-increment: subsec;}
h3::before {content: counter(chapter) "." counter(section) "."
        counter(subsec) ". ";}

这些规则将产生图 16-28 所示的效果。

css5 1628

图 16-28. 将计数器添加到标题

图 16-28 展示了关于计数器重置和增加的一些重要点。例如,请注意计数器在元素上重置,而实际生成内容的计数器是通过::before伪元素插入的。试图在伪元素中重置计数器是行不通的:你会得到很多零。

还要注意,<h1> 元素使用了默认为 0 的计数器 chapter,并在元素文本之前加上了“1.”。当一个计数器被同一个元素递增并使用时,递增会在计数器显示之前发生。类似地,如果一个计数器被重置并在同一个元素中显示,则重置会在计数器显示之前发生。考虑以下内容:

h1::before, h2::before, h3::before {
  content: counter(chapter) "." counter(section) "." counter(subsec) ". ";}
h1 {counter-reset: section subsec;
  counter-increment: chapter;}

文档中第一个 <h1> 元素会在文本“1.0.0.”之前显示,因为计数器 sectionsubsec 被重置但没有被递增。因此,如果你希望递增计数器的第一个显示实例为 0,你需要将该计数器重置为 -1,如下所示:

body {counter-reset: chapter -1;}
h1::before {counter-increment: chapter; content: counter(chapter) ". ";}

你可以使用计数器做一些有趣的事情。考虑以下 XML:

<code type="BASIC">
  <line>PRINT "Hello world!"</line>
  <line>REM This is what the kids are calling a "comment"</line>
  <line>GOTO 10</line>
</code>

你可以使用以下规则重新创建 BASIC 程序列表的传统格式:

code[type="BASIC"] {counter-reset: linenum; font-family: monospace;}
code[type="BASIC"] line {display: block;}
code[type="BASIC"] line::before {counter-increment: linenum 10;
  content: counter(linenum) ": ";}

你也可以在counter()格式的一部分定义每个计数器的列表样式。你可以在计数器标识符后添加逗号分隔的list-style-type关键字来实现这一点。标题计数器示例的以下修改在图 16-29 中有所体现:

h1 {counter-reset: section subsec;
    counter-increment: chapter;}
h1::before {content: counter(chapter,upper-alpha) ". ";}
h2 {counter-reset: subsec;
    counter-increment: section;}
h2::before {content: counter(chapter,upper-alpha)"." counter(section) ". ";}
h3 {counter-increment: subsec;}
h3::before {content: counter(chapter,upper-alpha) "." counter(section) "."
        counter(subsec,lower-roman) ". ";}

请注意,计数器 section 没有给定样式关键字,因此默认为十进制计数样式。如果你希望,你甚至可以设置计数器使用disccirclesquarenone样式,尽管这些计数器的每个实例都将是你指定的符号的单一副本。

有一个有趣的点需要注意,具有displaynone的元素不会递增计数器,即使规则似乎表明了另外一种情况。相反,具有visibilityhidden的元素确实会递增计数器:

.suppress {counter-increment: cntr; display: none;}
  /* 'cntr' is NOT incremented */
.invisible {counter-increment: cntr; visibility: hidden;}
  /* 'cntr' IS incremented */

css5 1629

图 16-29. 更改计数器样式

计数器和作用域

到目前为止,您已经看到如何将多个计数器串联在一起,以创建章节和子章节计数。通常,这也是作者希望对嵌套有序列表进行的操作,但是试图创建足够的计数器以涵盖深层嵌套级别会很快变得笨拙。仅仅是为了使计数器在五级深度嵌套列表中工作,就需要像这样的一堆规则:

ol ol ol ol ol li::before {
    counter-increment: ord1 ord2 ord3 ord4 ord5;
    content: counter(ord1) "." counter(ord2) "." counter(ord3) "."
        counter(ord4) "." counter(ord5) ".";}

想象一下,写足够的规则来涵盖最多 50 级嵌套!(我们并不是说您应该将有序列表嵌套到 50 层深。暂时跟随我们的思路。)

幸运的是,CSS 2.1 在涉及计数器时描述了作用域的概念。简单来说,每个嵌套级别都为任何给定的计数器创建一个新的作用域。作用域是使以下规则能够以通常的 HTML 方式覆盖嵌套列表计数的原因:

ol {counter-reset: ordered;}
ol li::before {counter-increment: ordered; content: counter(ordered) ". ";}

所有这些规则将使有序列表(甚至是嵌套在其他列表中的列表)从 1 开始计数,并逐项递增—这正是从 HTML 开始就一直做的方式。

这有效的原因是在每个嵌套级别都创建了ordered计数器的新实例。因此,对于第一个有序列表,创建了ordered的一个实例。然后,对于嵌套在第一个列表中的每个列表,都会创建另一个新实例,并且每个列表的计数从头开始。

然而,假设您希望有序列表的计数方式是每个嵌套级别都创建一个新的计数器附加到旧的计数器上:1、1.1、1.2、1.2.1、1.2.2、1.3、2、2.1,等等。这不能通过counter()完成,但是可以通过counters()完成。一个“s”能带来多大的不同啊。

要创建图 16-30 中显示的嵌套计数器样式,您需要这些规则:

ol {counter-reset: ordered; list-style: none;}
ol li:before {content: counters(ordered,".") ": "; counter-increment: ordered;}

css5 1630

图 16-30. 嵌套计数器

基本上,关键字counters(ordered,".")显示来自每个作用域的ordered计数器,并在其后附加一个句点,然后将给定元素的所有作用域计数器串在一起。因此,第三级嵌套列表中的项目将以最外层列表的ordered值作为前缀,外层和当前列表之间列表的作用域,以及当前列表的作用域,每个作用域后面都跟着一个句点。content值的其余部分导致所有这些计数器之后添加一个空格、冒号和空格。

counter()一样,您可以为嵌套计数器定义列表样式,但是相同的样式适用于所有计数器。因此,如果您将之前的 CSS 更改为以下内容,图 16-30 中的列表项将使用小写字母而不是数字来进行计数:

ol li::before {counter-increment: ordered;
    content: counters(ordered,".",lower-alpha) ": ";}

您可能已经注意到,在前面的示例中,<ol> 元素上应用了 list-style: none。这是因为插入的计数器是生成的内容,而不是替换列表标记。换句话说,如果省略了 list-style: none,每个列表项将具有其用户代理提供的列表计数器,加上 我们定义的生成内容计数器。

这种能力非常有用,但有时您确实只想重新定义标记本身。这就是计数模式的作用。

定义计数模式

如果你想深入了解简单的嵌套计数,也许是定义基于 60 进制计数或使用符号模式,CSS 提供了一种几乎可以想象的定义任何计数模式的方法。你可以使用 @counter-style 块,并有专门的描述符来管理结果。一般的模式如下:

@counter-style <*`name`*> {
    …declarations…
}

在这里,<*name*> 是问题模式的作者提供的名称。例如,要创建一系列交替的三角形标记,块可能看起来像这样:

@counter-style triangles {
    system: cyclic;
    symbols: ▶ ▷;
}
ol {list-style: triangles;}

图 16-31 显示了结果。

css5 1631

图 16-31. 一个简单的计数器模式

这里总结了几个可用的描述符。

我们将从简单系统开始,逐步提升复杂度,但首先,让我们看看两个最基本描述符的精确定义:systemsymbols

对于几乎所有的@counter-style块,这些是最基本的两个描述符。如果你定义的是一个symbolic系统,可以省略system,但通常最好包含它,这样你就清楚地了解你正在设置的系统类型。记住,下一个工作在样式上的人可能不像你那么熟悉计数器样式!

固定计数模式

最简单的计数器模式是一个 fixed 系统。当你希望定义一个精确的计数器标记序列,当标记用完后不重复时,就使用固定系统。考虑以下示例,其结果显示在 图 16-32 中:

@counter-style emoji {
    system: fixed;
    symbols:     ;
}
ol.emoji {list-style: emoji;}

css5 1632

图 16-32. 一个固定的计数器模式

一旦列表超过第五个列表项,计数器系统就会用完表情符号,而且由于没有定义回退(我们马上会谈到这个),随后列表项的标记就会回退到有序列表的默认标记。

注意,symbols 描述符中的符号是用空格分隔的。如果它们都紧密排列在一起而没有空格分隔,你会得到类似于 图 16-33 中的结果。

css5 1633

图 16-33. 当符号太接近时

这意味着你可以定义一个固定序列的标记,在这个序列中每个标记由多个符号组成。(如果你想定义一组符号,这些符号结合起来形成计数系统的模式,稍等:我们很快会讲到。)

如果你想在你的标记中使用 ASCII 符号,通常建议对它们进行引用。这可以避免解析器将尖括号误认为 HTML 的一部分而产生问题。因此,你可以做类似这样的事情:

@counter-style emoji {
    system: fixed;
    symbols: # $ % ">";
}

引用所有符号是可以接受的,而且养成这个习惯可能是个好主意。这意味着更多的打字——前面的值将变成"#", "$", "%", ">"——但是更少出错。

在固定计数系统中,你可以在system描述符本身定义一个起始值。例如,如果你想从 5 开始计数,你可以这样写:

@counter-style emoji {
    system: fixed 5;
    symbols:     ;
}
ul.emoji {list-style: emoji;}

在这种情况下,前五个符号表示计数器 5 到 9。

此功能仅在固定计数系统中可用。

循环计数模式

超越固定模式的下一个步骤是cyclic模式,这是重复的固定模式。让我们将前一节中的固定表情符号模式转换为循环模式。这将得到图 16-34 中显示的结果:

@counter-style emojiverse {
    system: cyclic;
    symbols:     ;
}

ul.emoji {list-style: emojiverse;}

css5 1634

图 16-34. 循环计数模式

定义的符号按顺序重复使用,直到没有剩余的项目需要计数。

可以使用cyclic提供一个用于整个模式的单个标记,就像为list-style-type提供字符串一样。在这种情况下,它可能看起来像这样:

@counter-style thinker {
    system: cyclic;
    symbols: ;
    /* equivalent to list-style-type: ; */
}

ul.hmmm {list-style: thinker;}

你可能已经注意到,到目前为止,我们所有的计数器都跟着一个句点(或者如果你更喜欢的话,是一个句号)。这是由于suffix描述符的默认值,它有一个类似的描述符prefix

通过这些描述符,你可以定义在模式中每个标记前后插入的符号。因此,我们可以给我们的思想家增加 ASCII“翅膀”,就像在图 16-35 中所示的那样:

@counter-style wingthinker {
    system: cyclic;
    symbols: ;
    prefix: "~";
    suffix: " ~";
}

ul.hmmm {list-style: wingthinker;}

css5 1635

图 16-35. 给思想家加上“翅膀”

suffix描述符在想要移除标记的默认后缀时特别有用。以下是如何进行的一个示例:

@counter-style thisisfine {
    system: cyclic;
    symbols:   ;
    suffix: "";
}

你还可以通过使用prefixsuffix以创造性的方式扩展标记,就像在图 16-36 中所示的那样:

@counter-style thisisfine {
    system: cyclic;
    symbols:   ;
    prefix: "";
    suffix: "";
}

css5 1636

图 16-36. 这个列表很好

你可能会想知道为什么在这个例子中prefix值被引用,而suffix值却没有。除了演示两种方法都可行之外,没有其他理由。如前所述,引用符号更安全,但很少需要。

在这里的 CSS 示例和图形中显示的 Unicode 图标可能存在一些差异。这是使用表情符号和其他字符时无法避免的一个方面——在一个人的用户代理上显示的内容可能在另一个人的上面有所不同。请考虑 macOS、iOS、Android、Samsung、Windows 桌面、Windows 移动、Linux 等系统中表情符号渲染的差异。

您可以理论上使用图像作为计数器。例如,假设您想使用一系列克林贡字形,这些字形没有 Unicode 等价物。(长期以来的行业传言称克林贡语已包含在 Unicode 中。该提案于 1997 年提出并于 2001 年被拒绝。2016 年又提出了新的提案,再次被拒绝。)我们不会在这里代表整个符号集,但它会以以下方式开始:

@counter-style klingon-letters {
    system: cyclic;
    symbols: url(i/klingon-a.svg) url(i/klingon-b.svg)
        url(i/klingon-ch.svg) url(i/klingon-d.svg)
        url(i/klingon-e.svg) url(i/klingon-gh.svg);
    suffix: url(i/klingon-full-stop.svg);
}

这将循环从AGH,然后重复,但仍然会得到一些克林贡符号,这可能已经足够了。我们将在本章后面看到如何建立字母和数字系统。

警告

截至 2022 年底,对于任何类型的<image>作为计数符号的浏览器支持基本上不存在。

象征性计数模式

象征性计数系统类似于循环系统,但在象征性系统中,对于每次符号序列的重启,符号数量会增加一个。每个标记由一个重复出现的单个符号组成,该符号序列重复的次数。这可能与您熟悉的脚注符号或某些种类的字母系统类似。这里展示了每种类型的示例,结果显示在图 16-37 中:

@counter-style footnotes {
    system: symbolic;
    symbols: "*" "†" "§";
    suffix: ' ';
}
@counter-style letters {
    system: symbolic;
    symbols: A B C D E;
}

css5 1637

图 16-37。两种象征性计数模式

要注意的一件事是,如果只有少数符号应用于非常长的列表,标记很快会变得非常长。请考虑前面示例中显示的字母计数器。图 16-38 显示了使用该系统的列表中第 135 至 150 个条目的样子。

css5 1638

图 16-38。非常长的象征性标记

从现在开始,这种考虑将变得更加重要,因为计数器样式在某种意义上都是累加的。为了限制您面临的这类问题,您可以使用range描述符。

使用range,您可以提供一个或多个用空格分隔的值对,每对值之间用逗号分隔。假设我们希望在三次迭代后停止字母加倍。我们有五个符号,因此我们可以将它们限制在前 15 个列表项中使用,如下所示,结果显示在图 16-39 中(为了保持图像的合理大小,已排列为两列):

@counter-style letters {
    system: symbolic;
    symbols: A B C D E;
    range: 1 15;
}

css5 1639

图 16-39。使用range限制象征性计数器模式

如果有必要,无论出于什么原因,我们需要提供第二个计数器使用范围,它将如下所示:

@counter-style letters {
    system: symbolic;
    symbols: A B C D E;
    range: 1 15, 101 115;
}

letters定义的象征性字母系统将适用于 1 至 15 以及 101 至 115 范围内(这将是“AAAAAAAAAAAAAAAAAAAAA”到“EEEEEEEEEEEEEEEEEEEEEEE”,相当合适)。

那么,对于超出range定义范围的计数器会发生什么?它们将回退到默认的标记样式。您可以让用户代理处理,或者通过fallback描述符提供一些指示。

例如,您可能决定用希伯来语计数来处理超出范围的计数器:

@counter-style letters {
    system: symbolic;
    symbols: A B C D E;
    range: 1 15, 101 115;
    fallback: hebrew;
}

您可以轻松地使用lower-greekupper-latin或者像square这样的非计数样式。

字母计数模式

一个alphabetic计数系统类似于symbolic系统,不同之处在于重复方式的变化。请记住,在符号计数中,随着每次迭代,符号的数量会增加。在字母系统中,每个符号被视为编号系统中的一个数字。如果您在电子表格中花费了一些时间,这种计数方法可能会使您对列标签感到熟悉。

为了说明这一点,让我们重用前一节中的字母符号,并从符号系统更改为字母系统。结果显示在图 16-40 中(再次以两列的形式进行格式化)。

@counter-style letters {
    system: alphabetic;
    symbols: A B C D E;
    /* once more cut off at 'E' to show the pattern’s effects more quickly */
}

css5 1640

图 16-40. 字母计数

注意模式的第二次迭代,它从“AA”到“AE”,然后切换到“BA”到“BE”,然后到“CA”等等。在符号版本中,到达字母系统中的“EE”时,我们已经到了“EEEEEE”。

请注意,为了有效,字母系统必须在symbols描述符中提供至少个符号。如果只提供一个符号,则整个@counter-style块将被视为无效。任何两个符号都是有效的;它们可以是字母、数字或者 Unicode 中的任何东西,以及(理论上)图像。

数字计数模式

当您定义一个numeric系统时,实际上是在使用您提供的符号定义位置计数系统——也就是说,这些符号被用作位数计数系统中的数字。例如,定义普通的十进制计数将会像这样完成:

@counter-style decimal {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
}

这种基数可以扩展为创建十六进制计数,如下所示:

@counter-style hexadecimal {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' 'A' 'B' 'C' 'D' 'E' 'F';
}

该计数样式将从 1 计数到 F,然后滚动到 10 并继续计数到 1F,然后 20 到 2F,30 到 3F,等等。更简单地说,设置二进制计数非常简单:

@counter-style binary {
    system: numeric;
    symbols: '0' '1';
}

这些三种计数模式的示例显示在图 16-41 中。

css5 1641

图 16-41. 三种数字计数模式

要考虑的一个有趣问题是:如果计数器值为负数会发生什么?在十进制计数中,我们通常期望负数前面带有减号(),但在其他系统中呢,比如符号系统呢?如果我们定义一个基于字母的数字计数系统呢?或者如果我们想使用会计风格的格式,将负值放入括号中呢?这就是negative描述符发挥作用的地方。

negative 描述符就像是其自身的小型自包含的 prefixsuffix 的组合,仅在计数器具有负值时应用。其符号放置在任何前缀和后缀符号的内部(即靠近计数器)。

因此,假设我们要使用会计风格的格式,并向所有计数器添加前缀和后缀符号。操作如下,结果显示在 图 16-42 中:

@counter-style accounting {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
    negative: "(" ")";
    prefix: "$";
    suffix: " - ";
}
ol.kaching {list-style: accounting;}
<ol start="-3">
…
</ol>

css5 1642

图 16-42. 负值格式化

数字计数系统的另一个常见特征是希望填充低值,使其长度与较高值相匹配。例如,计数模式可能使用前导零来创建 001 和 100,而不是 1 和 100。这可以通过 pad 描述符来实现。

此描述符的模式非常有趣。第一部分是一个整数,定义了每个计数器应具有的位数。第二部分是一个字符串,用于填充少于定义位数的任何值。考虑以下示例:

@counter-style padded {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
    suffix: '.';
    pad: 4 "0";
}

ol {list-style: decimal;}
ol.padded {list-style: padded;}

在这些样式下,有序列表默认使用十进制计数:1, 2, 3, 4, 5… 具有 classpadded 的列表将使用填充的十进制计数:0001, 0002, 0003, 0004, 0005… 图 16-43 展示了一个示例。

css5 1643

图 16-43. 填充值

请注意,填充计数器使用 0 符号来填补任何缺失的前导数字,以使每个计数器至少为四位数。该句中的“至少”部分很重要:如果计数器达到五位数,它将不会填充。更重要的是,如果计数器达到五位数,其他较短的计数器也不会获得额外的零。它们将保持四位数长,因为 4 "0" 中的 4

任何符号都可以用来填充值,不仅限于 0。您可以使用下划线、句点、表情符号、箭头符号、空格或其他任何喜欢的东西。事实上,在值的 <*symbol*> 部分中可以有多个字符。以下内容是完全可以接受的,尽管不一定是理想的:

@counter-style crazy {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
    suffix: '.';
    pad: 7 " ";
}

ol {list-style: decimal;}
ol.padded {list-style: padded;}

如果给定计数器值为 1,那么该疯狂计数系统的结果将是“thinking face emojiwinking face emojithinking face emojiwinking face emojithinking face emojiwinking face emoji1。”

请注意,负数符号计入符号长度,因此会影响填充。还要注意,负号将会在任何填充之外 外侧 出现。给定以下样式,我们将得到在 图 16-44 中显示的结果:

@counter-style negativezeropad {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
    suffix: '. ';
    negative: '–';
    pad: 4 "0";
}
@counter-style negativespacepad {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
    suffix: '. ';
    negative: '–';
    pad: 4 " ";
}

css5 1644

图 16-44. 带填充的负值格式化

累加计数模式

我们还有一个系统类型要探索,即additive-symbol计数。在加法计数系统中,使用不同的符号来表示值。正确地将多个符号组合在一起,然后将每个符号代表的数字相加,就得到了计数器的值。

显示比解释更容易。以下是从Kseso改编的示例:

@counter-style roman {
    system: additive;
    additive-symbols:
        1000 M, 900 CM, 500 D, 400 CD,
        100 C, 90 XC, 50 L, 40 XL,
        10 X, 9 IX, 5 V, 4 IV, 1 I;
}

这将以古典罗马风格计数。计数样式规范中还有一个很好的例子,它定义了一个骰子计数系统:

@counter-style dice {
    system: additive;
    additive-symbols: 6 ⚅, 5 ⚄, 4 ⚃, 3 ⚂, 2 ⚁, 1 ⚀, 0 "__";
    suffix: " ";
}

两种计数系统的结果如图 16-45 所示;这次,每个列表都已格式化为三列。

css5 1645

图 16-45。加法值

为了清晰起见,符号可以加引号;例如,6 "⚅", 5 "⚄", 4 "⚃"等等。

最重要的一点是记住符号的顺序及其等值的重要性。请注意,无论是罗马还是骰子计数系统都是从最大到最小提供值,而不是反过来。这是因为如果你不按降序放置值,整个块都将无效。

还请注意使用additive-symbols描述符而不是symbols。这很重要要记住,因为定义了加法系统,然后尝试使用symbols描述符将使整个counter-styles块无效。(同样,尝试在非additive系统中使用additive-symbols描述符也会使那些块无效。)

关于加法系统最后要注意的一点是,由于加法计数器算法的定义方式,有时候可能无法表示某些值,尽管看起来应该可以。考虑这个定义:

@counter-style problem {
    system: additive;
    additive-symbols: 3 "Y", 2 "X";
    fallback: decimal;
}

对于前五个数字,计数器的结果如下:1, X, Y, 4, YX。你可能会认为 4 应该是 XX,这样直观上似乎是有道理的,但加法符号的算法不允许这样做。引用规范的话说:“虽然很不幸,但这是为了保持算法相对于计数器值大小的线性时间。”

提示

那么罗马计数是如何获得 III 代表 3 的呢?答案还是在算法中。这里不太容易解释,如果你真的很好奇,我们建议你阅读 CSS 计数样式第 3 级规范,其中定义了加法计数算法。如果这不是你感兴趣的,那就记住:确保你有一个值等于1的符号,你就可以避免这个问题。

扩展计数模式

可能会有时候你只是想稍微改变一个现有的计数系统。例如,假设你想要将常规的十进制计数改为使用闭括号符号作为后缀,并填充最多两个前导零。你可以像下面这样详细写出来:

@counter-style mydecimals {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
    suffix: ") ";
    pad: 2 "0";
}

这样做虽然有效,但有些笨拙。不过,别担心:extends来帮忙了。

extends选项在某种程度上类似于系统类型,但只是在基于现有系统类型的基础上构建。前面的例子将用extends重写如下:

@counter-style mydecimals {
    system: extends decimal;
    suffix: ") ";
    pad: 2 "0";
}

这将使用从list-style-type熟悉的decimal系统,并稍作调整。因此,无需重新输入整个符号链。你只需调整选项,如下所示。

实际上,你只能调整选项:如果你试图在extends系统中使用symbolsadditive-symbols,整个@counter-style块将无效并被忽略。换句话说,符号无法被扩展。例如,你不能通过扩展十进制计数来定义十六进制计数。

然而,你可以根据不同的上下文变化十六进制计数。例如,你可以设置基本的十六进制计数,然后定义不同的显示模式,如下面的代码所示,并在图 16-46 中进行说明。

注意

每个列表跳过 19 到 253,多亏了一个列表项上的value="253"

@counter-style hexadecimal {
    system: numeric;
    symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' 'A' 'B' 'C' 'D' 'E' 'F';
}
@counter-style hexpad {
    system: extends hexadecimal;
    pad: 2 "0";
}
@counter-style hexcolon {
    system: extends hexadecimal;
    suffix: ": ";
}
@counter-style hexcolonlimited {
    system: extends hexcolon;
    range: 1 255; /* stops at FF */
}

css5 1646

图 16-46. 不同的十六进制计数模式

请注意,四种计数样式中的最后一种hexcolonlimited扩展了第三种hexcolon,而hexcolon本身扩展了第一种hexadecimal。在hexcolonlimited中,十六进制计数在FF(255)处停止,这要归功于range: 1 255;声明。

计数模式的讲解

尽管使用符号构建计数器很有趣,但对于 Apple 的 VoiceOver 或 JAWS 屏幕阅读器等朗读技术来说,结果可能会变得一团糟。例如,想象一下屏幕阅读器试图读取骰子计数或月相。为了帮助,speak-as描述符允许你定义一个听觉回退。

警告

截至 2022 年末,speak-as仅受 Mozilla 系浏览器支持。

让我们来反过来看值。使用<counter-style-name>,你可以定义一个备选的计数样式,用户代理很可能已经识别。例如,当被朗读时,你可能希望提供一个骰子计数的音频备用作为decimal,这是一个广受支持的list-style-type值之一:

@counter-style dice {
    system: additive;
    speak-as: decimal;
    additive-symbols: 6 ⚅, 5 ⚄, 4 ⚃, 3 ⚂, 2 ⚁, 1 ⚀;
    suffix: " ";
}

鉴于这些样式,计数⚅⚅⚂会被朗读为“十五”。或者,如果speak-as值更改为lower-latin,该计数将被朗读为“oh”(大写字母O)。

spell-out值可能看起来相当简单,但比起初看起来要复杂一些。用户代理所拼写出来的是一个“计数表示”,然后逐字母拼写。很难预测这意味着什么,因为生成计数表示的方法并没有精确定义:规范中说,“计数表示是通过连接计数符号构造的”。就是这样。

words 值与 spell-out 类似,但计数的表示方式是以单词的形式而不是拼写出每个字母。再次强调,确切的过程未定义。

使用 numbers 值时,计数器以文档语言的数字形式发音。这与前面的代码示例类似,在英文文档中,⚅⚅⚂会被朗读为“fifteen”。如果是其他语言,将使用该语言进行计数:例如西班牙语中是“quince”,德语中是“fünfzehn”,中文中是“十五”等。

对于 bullets,用户代理在读取无序列表中的项目符号(标记)时会做出相应反应。这可能意味着根本不说话,或者产生如叮当声或点击声等音频提示。

最后,考虑 auto 的默认值。我们将此作为最后一个问题,因为其效果取决于正在使用的计数系统。如果是字母系统,则 speak-as: auto 的效果与 speak-as: spell-out 相同。在循环系统中,autobullets 相同。否则,效果与 speak-as: numbers 相同。

这个规则的例外情况是系统是 extends 系统的情况下,此时 auto 的效果基于系统的扩展情况而定。因此,根据以下样式,在 emojibrackets 列表中,计数器将被朗读,就好像 speak-as 被设置为 bullets 一样:

@counter-style emojilist {
    emojiverse {
    system: cyclic;
    symbols: ;
@counter-style emojibrackets {
    system: extends emojilist;
    suffix: "]] ";
    speak-as: auto;
}

总结

即使列表样式不如我们所希望的那样复杂,但样式化列表的能力仍然非常有用。一个相对常见的用法是将链接列表化,移除标记和缩进,从而创建一个导航侧边栏。简单的标记语言结合灵活的布局方式,难以抗拒。

请记住,如果标记语言没有固有的列表元素,生成内容可以提供巨大的帮助,比如插入指向特定类型链接(如 PDF 文件、Word 文档,甚至只是链接到另一个网站)的图标等内容。生成内容还可以轻松打印链接 URL,并且它插入和格式化引号的能力将带来真正的排版乐趣。可以肯定的是,生成内容的有用性仅限于您的想象力。更好的是,借助计数器,您现在可以将顺序信息关联到通常不是列表的元素,例如标题或代码块。如果您希望通过模仿用户操作系统外观来支持这些功能,请继续阅读。下一章将讨论如何更改设计的放置位置、形状甚至透视效果。

第十七章:Transforms

自 CSS 诞生以来,元素一直是矩形的,并且严格在水平和垂直轴上定位。出现了一些技巧使元素看起来像是倾斜等,但在所有这些技巧的背后都是一个严格的网格。

使用 CSS transforms,您可以打破视觉网格并改变元素呈现的方式。

坐标系

无论是简单地稍微旋转一些照片以使它们看起来更自然,还是创建信息可以通过翻转元素显示的界面,或者在侧边栏中进行有趣的透视技巧,CSS transforms 都能——如果您允许这个显而易见的表达——转变您设计的方式。

在踏上这段旅程之前,让我们花一点时间来定位自己。具体来说,让我们回顾一下用于定义空间中位置或运动的坐标系作为一系列测量的两种类型的坐标系。在 transforms 中使用的第一种是笛卡尔坐标系,通常称为x/y/z 坐标系。该系统通过使用两个数字(用于二维放置)或三个数字(用于三维放置)描述空间中点的位置。在 CSS 中,该系统使用三个轴:x 轴(水平)、y 轴(垂直)和 z 轴(深度)。这在 Figure 17-1 中有所说明。

css5 1701

图 17-1。CSS transforms 中使用的三个笛卡尔坐标轴

对于任何二维(2D)变换,您只需关心 x 和 y 轴。按照惯例,正 x 值向右移动,负值向左移动。同样地,正 y 值沿 y 轴向下移动,而负值沿 y 轴向上移动。

这可能看起来有点奇怪,因为我们倾向于认为更高的数字应该使某些东西放置得更高,而不是更低,就像我们在初代数中学到的一样。(这就是为什么在 Figure 17-1 中,“y”标签位于 y 轴的底部的原因:标签在所有三个轴上的正方向上。)如果您在 CSS 中有绝对定位的经验,请考虑绝对定位元素的top属性值:对于正值的top值,它们向下移动,而当top具有负长度时,它们向上移动。

鉴于此,为了将元素向左和向下移动,您需要给出一个负 x 值和一个正 y 值。这是一个实现方式:

translateX(-5em) translateY(33px)

事实上,这是一个有效的 transform 值,您很快会看到。它的效果是按顺序将元素向左移动 5 ems 并向下移动 33 像素。

如果您想要在三维空间中进行转换,需要添加一个 z 轴值。这个轴是“伸出”显示器并直接穿过您头部的轴——在理论上是这样的。正 z 值更接近您,负 z 值则远离您。在这方面,它非常像z-index属性。

假设我们要取之前移动的元素,并添加一个 z 轴值:

translateX(-5em) translateY(33px) translateZ(200px)

现在该元素看起来比没有 z 值时更接近我们 200 像素。

嗯,您可能会想知道如何将元素移动 200 像素靠近您,考虑到全息显示器很少见且昂贵。在您和显示器之间的空气分子等效于 200 像素有多少?元素靠近您看起来是什么样子,如果它变得靠近会发生什么?这些都是我们稍后会讨论的绝佳问题。现在,只需接受沿 z 轴移动元素似乎会使其看起来更接近或更远即可。

非常重要的一点要记住,每个元素都带有自己的参考框架,因此考虑其轴与自身的关系。如果旋转一个元素,轴也会随之旋转,如第 17-2 图所示。任何进一步的转换都是相对于那些旋转后的轴计算的,而不是显示的轴。

css5 1702

第 17-2 图。参考基本框架

现在,假设您想在显示平面(即绕 z 轴)顺时针旋转 45 度一个元素。这是您最有可能使用的转换值:

rotate(45deg)

将其更改为–45deg,元素将逆时针绕 z 轴旋转(对于我们的国际朋友则是逆时针)。换句话说,它将在xy平面内旋转,如第 17-3 图所示。

css5 1703

第 17-3 图。xy 平面上的旋转

谈到旋转,CSS 转换中使用的另一种坐标系是球坐标系,它描述了三维空间中的角度。它在第 17-4 图中有示例。

css5 1704

第 17-4 图。CSS 转换中使用的球坐标系

对于 2D 转换,您只需担心一个单独的 360 度极坐标系:即坐落在由 x 轴和 y 轴描述的平面上的坐标系。在旋转方面,2D 旋转实际上描述了绕 z 轴的旋转。类似地,绕 x 轴的旋转会使元素向您倾斜或远离您,而绕 y 轴的旋转则会使元素从一侧转向另一侧。这些在第 17-5 图中有示例。

css5 1705

第 17-5 图。绕三个轴旋转

好了,既然我们有了方向,那么让我们开始使用 CSS 转换吧!

转换中

一个属性将所有变换作为单个操作应用,而一些辅助属性则影响如何精确应用变换,或者允许以一种单一的方式进行变换。我们将从最重要的开始。

<transform-list> 是定义不同变换的函数的以空格分隔的列表,就像在前面一节中使用的示例一样。我们稍后将详细讨论可以在其中使用的具体函数。

首先,让我们澄清边界框的问题。对于任何受 CSS 影响的元素,边界框 是边框框——元素边框的最外边缘。为了计算边界框,任何轮廓和边距都将被忽略。

注意

如果正在变换表格显示元素,则其边界框是表格包装框,它包围表格框和任何相关联的标题框。

如果您正在用 CSS 变换 SVG 元素,则其边界框是其 SVG 定义的 对象边界框

请注意,所有变换的元素(例如,将 transform 设置为除 none 以外的值的元素)都有自己的堆叠上下文。(有关说明,请参见 “Z 轴上的放置”。)

尽管经过变换后的缩放元素可能比之前大或小得多,但元素在页面上实际占据的空间与变换应用前相同。这对所有变换函数都是适用的:当您平移或旋转元素时,其兄弟元素不会自动让开。

现在,值输入 <transform-list> 需要一些解释。它指的是一个或多个变换函数的列表,一个接一个地以空格分隔的格式。它看起来像这样,其结果显示在 Figure 17-6 中:

#example {transform: rotate(30deg) skewX(-25deg) scaleY(2);}

css5 1706

图 17-6. 一个变换的 <div> 元素

函数逐一处理,从最左边的第一个开始,依次到最右边的最后一个。这种从头到尾的处理顺序非常重要,因为改变顺序可能导致截然不同的结果。请考虑以下两个规则,其结果显示在 Figure 17-7 中:

img#one {transform: translateX(200px) rotate(45deg);}
img#two {transform: rotate(45deg) translateX(200px);}

css5 1707

图 17-7. 不同的变换列表,不同的结果

在第一个示例中,图像沿其 x 轴平移(移动)了 200 像素,然后旋转了 45 度。在第二个示例中,图像旋转了 45 度,然后沿其 x 轴移动了 200 像素——这是变换元素的 x 轴,而不是 父元素、页面或视口的 x 轴。换句话说,当一个元素被旋转时,它的 x 轴(以及其它轴)会随之旋转。所有元素变换都是相对于元素自身的参考系进行的。

请注意,当您有一系列变换函数时,它们必须全部正确格式化;即它们必须是有效的。如果有一个函数无效,那么整个值就会无效。请考虑以下情况:

img#one {transform: translateX(100px) scale(1.2) rotate(22);}

因为rotate()的值是无效的—旋转值必须是一个<angle>—整个值被丢弃了。所讨论的图像将仅保持其初始未转换状态,既没有平移也没有缩放,更不用说旋转了。

此外,变换通常不是累积的。如果你对一个元素应用了一个变换,然后稍后想添加一个变换,你需要重新声明原始的变换。考虑以下场景,见图 17-8:

#ex01 {transform: rotate(30deg) skewX(-25deg);}
#ex01 {transform: scaleY(2);}
#ex02 {transform: rotate(30deg) skewX(-25deg);}
#ex02 {transform: rotate(30deg) skewX(-25deg) scaleY(2);}

css5 1708

图 17-8. 覆盖或修改变换

在第一种情况下,第二条规则完全取代了第一条,这意味着元素仅沿着 y 轴进行缩放。这有些合理;这就像你声明一个字体大小,然后在其他地方为同一个元素声明了不同的字体大小一样。你不会得到累积的字体大小。你只会得到其中一个大小。在第二个例子中,第一组变换的全部内容包含在第二组中,因此它们都与scaleY()函数一起应用。

注意

如果你希望应用于单一类型变换的属性,例如仅旋转或仅缩放元素的属性,你将在本章的后面看到一些例子,所以请耐心等待。

这里有一个重要的警告:截至本文写作时,变换通常不应用于原子内联级框。这些是像 span、超链接等内联框。这些元素可以在它们的块级父元素进行变换时进行变换,这样它们就随之而动。但你不能仅仅旋转一个 <span>,除非你通过 display: blockdisplay: inline-block 或类似的方式改变了它的显示角色。这种限制的原因归结为不确定性。假设你有一个 <span>(或任何内联级框),它跨越多行。如果你旋转它,会发生什么?每一行框是否独立旋转,还是所有行框作为一个整体旋转?没有明确的答案,争论仍在进行中,因此目前你不能直接转换内联级框。

变换函数

截至 2023 年初,CSS 共有 21 个变换函数,使用各种值模式来完成它们的工作。以下是所有可用变换函数的列表,减去它们的值模式:

| translate() translate3d()

translateX()

translateY()

translateZ() | scale() scale3d()

scaleX()

scaleY()

scaleZ() | rotate() rotate3d()

rotateX()

rotateY()

rotateZ() | skew() skewX()

skewY() | matrix() matrix3d()

perspective() |

我们将首先解决最常见的变换类型,以及它们的相关属性(如果存在),然后再处理更晦涩或更难的部分。

平移

平移变换 只是沿一个或多个轴的移动。例如,translateX() 沿其自身的 x 轴移动元素,translateY() 沿其 y 轴移动它,而 translateZ() 则沿其 z 轴移动它。

这些通常被称为二维 平移函数,因为它们可以将元素向上下或左右滑动,但不能沿着 z 轴向前或向后移动。每个函数接受一个距离值,表示为长度或百分比。

如果值是一个长度,则效果与预期相符。使用 translateX(200px) 可以将元素沿 x 轴移动 200 像素,它将向右移动 200 像素。将其改为 translateX(-200px),元素将向左移动 200 像素。对于 translateY(),正值将使元素向下移动,而负值将使其向上移动。

请记住,平移始终是相对于元素本身声明的。因此,例如,如果你通过旋转将元素倒置,正的 translateY() 值将使元素在页面上向下移动,因为从倒置元素的角度来看,这是向上移动。

如果值是一个百分比,则距离按元素自身大小的百分比计算。因此,如果一个元素宽度为 300 像素,高度为 200 像素,则 translateX(50%) 将使其向右移动 150 像素,而 translateY(-10%) 将使该元素向上移动 20 像素(相对于自身而言)。

如果你想同时沿 x 轴和 y 轴平移一个元素,translate() 使得这很容易。只需首先提供 x 值,然后是 y 值,用逗号分隔,这与包含 translateX()translateY() 的效果相同。如果省略 y 值,则假定为 0。因此,translate(2em) 被视为 translate(2em,0),这也与 translateX(2em) 相同。参见 图 17-9 了解二维平移的一些示例。

css5 1709

图 17-9. 二维平移

translateZ() 函数沿 z 轴平移元素,从而将其移入第三维。与二维平移函数不同,translateZ() 仅接受长度值。不允许使用百分比值用于 translateZ(),实际上也不允许用于任何 z 轴值。

就像 translate() 对于 x 和 y 的转换一样,translate3d() 是一个快捷函数,将 x、y 和 z 的转换值合并为一个函数。如果你想一次性向前、向上和向前移动一个元素,这非常方便。

见 图 17-10 了解 3D 平移工作原理的插图。每个箭头代表沿该轴的平移,到达三维空间中的一个点。虚线显示了从原点(三个轴的交点)到达的距离和方向,以及在 xy 平面上方的距离。

translate()不同,如果translate3d()不包含三个值,那么没有回退选项。因此,浏览器应将translate3d(1em,-50px)视为无效,因为不会发生实际的平移。

css5 1710

图 17-10. 三维平移

平移属性

当您想要对元素进行平移而无需通过transform属性时,可以使用translate属性代替。

translate()函数非常类似,translate属性接受从一个到三个长度值,或两个百分比和一个长度值,或更简化的模式,如单个长度。与translate()函数不同,transform属性不使用逗号分隔其值。

如果只提供一个值,则用作 x 轴的平移。当提供两个值时,第一个是 x 轴的平移,第二个是 y 轴的平移。当提供三个值时,它们按顺序x y z取值。任何缺失的值默认为0px

如果您回顾图 17-9,以下示例将产生与其所示相同的结果:

translate: 25px;    /* equivalent to 25px 0px 0px */
translate: 25%;
translate: 0 25px;  /* equivalent to 0 25px 0px */
translate: 0 -25px;
translate: 20% 20%;
translate: -20% -20%;
translate: 110% 25px;

类似地,以下示例将产生与图 17-10 中所示相同的效果:

translate: 150px -50px 100px;

默认值为none,意味着不应用任何翻译。

缩放

缩放变换会使元素变大或变小,具体取决于您提供的值。这些值是无单位的实数,可以是正数或负数。在二维平面上,您可以分别沿 x 轴和 y 轴缩放,也可以同时缩放它们。

提供给缩放函数的数字值是一个乘数;因此,scaleX(2)将使元素比转换之前宽出两倍,而scaleY(0.5)将使其高度减少一半。百分比值等同于数字值的比例为 100:1;也就是说,50%将产生与0.5相同的效果,200%将产生与2相同的效果,依此类推。

如果您想要同时沿两个轴缩放,请使用scale()。x 值始终在前,y 值始终在后,因此scale(2,0.5)将使元素的宽度增加一倍,高度减少一半。如果只提供一个数字,则用作两个轴的缩放值;因此,scale(2)将使元素的宽度和高度都增加一倍。这与translate()形成对比,其中省略的第二个值总是被设置为 0。使用scale(1)将会使元素缩放为与缩放之前完全相同的大小,如同scale(1,1)一样—假如您想要这样做。

图 17-11 展示了使用单轴缩放函数以及组合scale()的几个示例。

如果可以在两个维度上进行缩放,也可以在三个维度上进行缩放。CSS 提供了 scaleZ() 用于仅沿 z 轴缩放,以及 scale3d() 用于同时沿三个轴缩放。这仅在元素具有任何深度时有效,默认情况下元素没有深度。如果确实进行了传达深度的更改,比如围绕 x 轴或 y 轴旋转元素,则可以缩放深度,scaleZ()scale3d() 可以这样做。

css5 1711

图 17-11. 缩放元素

translate3d() 类似,scale3d() 函数要求所有三个数字都有效。如果未能做到这一点,格式错误的 scale3d() 将使其所属的整个变换值无效。

还要注意,缩放元素会改变任何平移的有效距离。例如,以下内容将导致元素向右平移 50 像素:

transform: scale(0.5) translateX(100px);

这是因为元素缩小了 50%,然后在其自身的参考框架内向右移动了 100 像素,其大小为原始的一半。交换函数的顺序,元素将首先向右移动 100 像素,然后从该位置缩小 50%。

缩放属性

与平移类似,scale 属性允许你在不使用 transform 属性的情况下对元素进行缩放。

scale 处理其值的方式与 translate 属性几乎没有区别。如果只给出一个值,例如 scale(2),则该值用于在 x 和 y 方向上进行缩放。使用两个值时,第一个用于在 x 轴方向上进行缩放,第二个用于在 y 轴方向上进行缩放。使用三个值时,第三个用于在 z 轴方向上进行缩放。

下列结果与 图 17-11 所示相同。

scale: 2 1;   /* equivalent to 200% 100% */
scale: 0.5 1; /* equivalent to 50% 100% */
scale: 1 2;
scale: 1 0.5;
scale: 1.5;
scale: 1.5;
scale: 0.5 1.5;
scale: 1 5 0.5;

默认值 none 表示不应用任何缩放。

元素旋转

旋转函数会导致元素围绕轴或三维空间中的任意向量旋转。CSS 提供了四个简单的旋转函数,以及一个专门用于三维的较复杂函数。

所有四个基本的旋转函数只接受一个值:角度。这可以用一个数字表示,可以是正数或负数,并且可以使用任何有效的角度单位(deggradradturn)。(更多详情请参见 “角度”。)如果值的数字超出了给定单位的通常范围,则它看起来就像是给定了允许范围内的值。换句话说,437deg 的值会倾斜,就像它是 77deg-283deg 一样。

但请注意,这些仅在视觉上等效,前提是不以某种方式对旋转进行动画处理。也就是说,对1100deg的旋转进行动画处理将使元素在倾斜 20 度(或者如果你愿意,340 度)之前多次旋转。相比之下,对-20deg的旋转进行动画处理将使元素稍微向左倾斜,没有旋转;而对340deg的旋转进行动画处理将使元素几乎完全向右旋转。这三种动画都会达到相同的最终状态,但是每种情况下的过程却截然不同。

rotate()函数是直接的 2D 旋转函数,是你最有可能使用的。它在视觉上等效于rotateZ(),因为它使元素围绕 z 轴旋转。类似地,rotateX()会导致围绕 x 轴旋转,从而使元素朝向你或远离你倾斜;而rotateY()则围绕其 y 轴旋转元素,就像它是一个门一样。这些都在图 17-12 中有所说明。

css5 1712

图 17-12. 绕三个轴的旋转
警告

图 17-12 中的几个示例展示了完全 3D 的外观。这是通过某些transform-styleperspective属性值实现的,详见“选择 3D 风格”和“改变透视”,这里省略以确保清晰。在本文的任何时候,只要出现完全三维变换的元素,这一点都很重要。需要记住,如果仅尝试应用所示的变换函数,不会得到与图示相同的视觉效果。

如果你对向量感到舒适,并希望通过 3D 空间旋转元素,那么rotate3d()就适合你。前三个数指定了 3D 空间中向量的 x、y 和 z 分量,度数值(角度)决定了围绕声明的 3D 矢量旋转的量。

要从基本示例开始,rotateZ(45deg)的 3D 等效函数是rotate3d(0,0,1,45deg)。这指定了在 x 轴和 y 轴上为零的矢量,以及在 z 轴上为 1 的矢量;换句话说,就是 z 轴。元素因此围绕该矢量旋转了 45 度,如图 17-13 所示。该图还显示了围绕 x 轴和 y 轴旋转元素 45 度所需的适当rotate3d()值。

css5 1713

图 17-13. 绕 3D 矢量旋转

更复杂一点的是像rotate3d(-0.95,0.5,1,45deg)这样的情况,其中描述的向量指向 3D 空间中的轴之间。为了理解其工作原理,让我们从一个基本示例开始:rotateZ(45deg)(见图 17-13)。其等效于rotate3d(0,0,1,45deg)。前三个数字描述了一个向量的分量,该向量在 x 或 y 方向上没有大小,而在 z 方向上的大小为 1。因此,它沿着 z 轴正方向指向观察者。然后,元素顺时针旋转,当你朝向向量的起点时。

类似地,rotateX(45deg)的 3D 等效是rotate3d(1,0,0,45deg)。向量沿着 x 轴正方向(向右)指向。如果您站在该向量的末端并朝向其起点看,您将顺时针旋转 45 度。因此,从通常的观察者位置来看,元素的顶部远离观察者,底部朝向观察者旋转。

现在让我们稍微复杂化这个例子:假设你有rotate3d(1,1,0,45deg)。当在您的显示器上查看时,描述的向量从左上角到右下角,通过元素中心穿过(默认情况下;稍后我们将看到如何更改)。因此,元素的矩形形状有一条线在 45 度角度上,实际上是在其上穿过。然后,向量旋转 45 度,将元素随之旋转。当你向向量起点望回去时,旋转是顺时针的,所以元素的顶部远离观察者旋转,而底部朝向观察者旋转。如果我们将旋转改为rotate3d(1,1,0,90deg),那么元素将面向观察者,以 45 度角倾斜并朝右上方。

好的,有了这一切,现在试着想象如何确定rotate3d(-0.95,0.5,1,45deg)的向量。假设一个边长为 200 像素的立方体,向量的分量为沿 x 轴向左 190 像素,沿 y 轴向下 100 像素,沿 z 轴向观察者 200 像素。图 17-14 展示了该向量及向观察者呈现的最终结果。

因此,向量就像穿过正在旋转的元素的金属棒。当我们沿着向量的线望回去时,旋转是顺时针 45 度。但由于向量指向左、下和前方,这意味着元素的左上角朝向观察者旋转,而右下角朝远离观察者旋转,如图 17-14 所示。

css5 1714

图 17-14. 围绕 3D 向量的旋转及其向量的确定

为了更加清楚,rotate3d(1,1,0,45deg) 等同于 rotateX(45deg) rotateY(45deg) rotateZ(0deg)!这是一个容易犯的错误,包括你的谦逊通讯者在内。看起来它应该是等效的,但实际上并不是。如果我们将该向量放置在前面提到的虚拟 200×200×200 的立方体内,旋转轴将从原点指向右 200 像素和下 200 像素的点(200, 200, 0)。

这样做后,旋转轴将从左上到右下穿过元素,以 45 度角度。元素然后顺时针围绕该对角线旋转 45 度,当你向其原点(左上角)回望时,将右上角的元素角度向左移动,并稍微向左移动,而左下角的元素则向右移动。这与 rotateX(45deg) rotateY(45deg) rotateZ(0deg) 的结果有明显不同,如图 17-15 所示。

css5 1715

图 17-15。围绕 3D 轴旋转和依序围绕三个不同轴旋转的区别

旋转属性

与平移和缩放一样,CSS 有一个rotate属性,允许你围绕各种轴旋转元素,而无需使用transform属性。然而,使其可能的值语法有些不同。

有效值被划分为三种互斥的语法选项。最简单的是none的默认值意味着不应用旋转。

如果你想围绕单个轴旋转,最简单的方法是给出轴标识符以及你想旋转的角度。在以下代码中,每行包含了围绕给定轴旋转元素的两种等效方式:

transform: rotateX(45deg);    rotate: x 45deg;
transform: rotateY(33deg);    rotate: y 33deg;
transform: rotateZ(-45deg);   rotate: z -45deg;
transform: rotate(90deg);     rotate: 90deg;

最后一行与前面讨论过的rotate()函数的处理类似:使用单个度数值进行旋转是在xy平面上的 2D 旋转。(参见图 17-12 进行复习。)

如果你想定义一个 3D 向量作为旋转轴,rotate的值看起来会有些不同。例如,假设我们想围绕向量 -0.95, 0.5, 1 旋转一个元素 45 度,如图 17-14 所示。以下两种声明中的任何一种都会产生这种效果:

transform: rotate3d(-0.95, 0.5, 1, 45deg);
rotate: -0.95 0.5 1 45deg;

如果你愿意,你可以使用这种模式来围绕基本轴进行旋转;也就是说,rotate: z 23degrotate: 0 0 1 23deg 将产生相同的效果(就像 rotate: 23deg 一样)。在通过 JavaScript 更改旋转向量时,这可能很有用,但在其他情况下很少有用。

注意,transform具有rotate无法复制的能力:链式旋转。例如,transform: rotateZ(20deg) rotateY(30deg)将首先围绕 z 轴旋转 20 度,然后对该旋转结果围绕 y 轴旋转。rotate属性只能执行其中一种操作。获得相同结果的唯一方法是找出能使元素保持与transform操作相同状态的向量和角度。当然,有数学方法可以做到这一点,但这超出了本书的范围(尽管请参见“矩阵函数”)。

个人转换属性顺序

当使用单独的变换属性时,效果总是按照translate、然后rotate、然后scale的顺序应用。以下两条规则在功能上是等效的:

#mover {
	rotate: 30deg;
	scale: 1.5 1;
	translate: 10rem;}

#mover {
	transform: translate(10rem) rotate(30deg) scale(1.5, 1);
}

这很重要,因为例如,先平移再旋转与先旋转再平移是完全不同的。如果你需要使元素的变换按照非 transform-rotate-scale 的顺序发生,请使用transform而不是单独的属性。

倾斜

当你倾斜一个元素时,你沿着 x 轴和/或 y 轴倾斜它。没有 z 轴或 3D 倾斜。

在两种情况下,提供一个角度值,元素就会倾斜以匹配该角度。展示倾斜要比用文字解释要容易得多,因此图 17-16 展示了沿 x 轴和 y 轴的倾斜示例。

css5 1716

图 17-16. 沿 x 轴和 y 轴倾斜

使用skew(a,b)与使用skewX(a)skewY(b)不同。前者使用矩阵操作[ax,ay]指定 2D 倾斜。图 17-17 展示了这种矩阵倾斜的示例,以及它们与外观相似但实际上不同的双倾斜变换的区别。

警告

由于多种原因,包括skew(a,b)skewX(a) skewY(b)不同的方式,CSS 规范明确建议避免使用skew()。如果可能的话,你应该尽量避免使用它;我们在这里记录它,以防你在遗留代码中遇到它。

css5 1717

图 17-17. 倾斜元素

如果提供两个值,x 轴倾斜角总是在前,y 轴倾斜角在后。如果省略 y 轴倾斜角,则视为 0。

注意

不像平移、旋转和缩放那样,截至 2022 年末,CSS 没有skew属性,因此任何倾斜都必须通过transform属性进行管理。

矩阵函数

如果你是高级数学爱好者,或者喜欢从华克斯兄弟电影中得到的陈旧笑话,那么矩阵函数将成为你的最爱。要明确一点,CSS 没有matrix属性。

在 CSS 转换规范中,我们发现了matrix()的尖锐描述,它是一个以“六个值a-f的变换矩阵形式”指定 2D 变换的函数。

首先要明确一件事:一个有效的matrix()值是一个由六个逗号分隔的数字组成的列表。 不能多,不能少。 值可以是正数或负数。 其次,该值描述了元素的最终转换状态,将所有其他变换类型(旋转、倾斜等)合并为一个紧凑的语法。 第三,很少有人使用这种语法自己编写代码,尽管它经常由绘图或动画软件生成。

我们不打算详细讨论进行矩阵数学运算的复杂过程。 对于大多数读者来说,这将是一堵显而易见的胡言乱语的墙壁; 对于其他人来说,这将是在熟悉的领域上浪费时间。 您当然可以在线研究矩阵计算的复杂性,并鼓励任何有兴趣的人这样做。 我们只看看 CSS 中的语法和用法的基础知识。

这是它的简要介绍。 假设你将这个函数应用于一个元素:

matrix(0.838671, 0.544639, -0.692519, 0.742636, 6.51212, 34.0381)

这就是用来描述这个变换矩阵的 CSS 语法:

0.838671    -0.692519   0   6.51212
0.544639     0.742636   0   34.0381
0            0          1   0
0            0          0   1

是的。 那么这是做什么? 它的结果显示在图 17-18 中,这与编写以下内容的结果完全相同:

rotate(33deg) translate(24px,25px) skewX(-10deg)

css5 1718

图 17-18。一个经过矩阵变换的元素及其功能等效

问题实质上是,如果你熟悉或需要使用矩阵计算,你可以且应该使用。否则,你可以将更多可读性强的变换函数链接在一起,并将元素转换到相同的最终状态。

现在,那是关于简单的二维变换。 如果你想使用一个矩阵通过三个维度进行变换,那该怎么办?

再次,仅仅是为了好玩,我们来品味一下来自 CSS 变换规范的matrix3d()的定义:“指定一个 4×4 的齐次矩阵作为三维变换的参数,这个矩阵由 16 个以列为主序列的值组成。” 这意味着matrix3d()的参数必须是一个由 16 个逗号分隔的数字组成的列表,不多不少。 这些数字按列顺序排列在 4×4 的网格中,因此矩阵的第一列由值中的第一组四个数字形成,第二列由第二组四个数字形成,依此类推。 因此,你可以使用以下函数,

matrix3d(
    0.838671, 0, -0.544639, 0.00108928,
    -0.14788, 1, 0.0960346, -0.000192069,
    0.544639, 0, 0.838671, -0.00167734,
    20.1281, 25, -13.0713, 1.02614)

并将其写成这个矩阵:

  0.838671   -0.14788        0.544639     20.1281
  0           1              0            25
 -0.544639    0.0960346      0.838671    -13.0713
  0.00108928 -0.000192069   -0.00167734   1.02614

两者都有一个等效的最终状态,如下所示,这在图 17-19 中显示。

perspective(500px) rotateY(33deg) translate(24px,25px) skewX(-10deg)

css5 1719

图 17-19。一个经过matrix3d()变换的元素及其功能等效

关于最终状态等效的说明

需要记住的是,matrix()函数的最终状态及其等效的转换函数链只能被认为是相同的。这与“元素旋转”讨论的原因相同:因为393deg的旋转角度最终显示的旋转与33deg的角度相同。如果你正在对变换进行动画处理,这一点很重要,因为前者将导致元素在动画中进行滚筒翻滚,而后者不会。matrix()版本的最终状态也不会包含滚筒翻滚,而是始终使用最短可能的旋转达到最终状态。

为了说明这意味着什么,考虑以下,一个转换链及其matrix()等效形式:

rotate(200deg) translate(24px,25px) skewX(-10deg)
matrix(-0.939693, -0.34202, 0.507713, -0.879385, -14.0021, -31.7008)

注意 200 度的旋转。我们自然地将其解释为顺时针旋转 200 度,这也确实如此。然而,如果这两个转换进行动画处理,它们将表现出不同的效果:串联函数版本将确实顺时针旋转 200 度,而matrix()版本将逆时针旋转 160 度。两者最终会到达同一个位置,但是它们的到达方式不同。

即使在你可能认为它们不会有差异的情况下,也可能出现其他差异。再次强调,这是因为matrix()转换总是采用最短可能的路线到达最终状态,而转换链可能不会。(事实上,它可能不会。)考虑这些表面上等效的转换:

rotate(160deg) translate(24px,25px) rotate(-30deg) translate(-100px)
matrix(-0.642788, 0.766044, -0.766044, -0.642788, 33.1756, -91.8883)

如常,它们最终到达同一个地方。然而,在动画时,元素会采用不同的路径达到该最终状态。乍一看可能不明显有差异,但差异确实存在。

如果你不对转换进行动画处理,这一切都无关紧要,但仍然是一个重要的区别,因为你永远不知道何时会决定开始进行动画处理。(希望在阅读第十八章和第十九章后!)

设置元素透视

如果你在三维空间中转换一个元素,很可能希望它具有一些透视效果。透视可以使得前后深度显现,你可以调整应用到元素的透视程度。

指定透视作为一个距离可能有点奇怪。毕竟,perspective(200px)看起来很奇怪,因为你实际上不能沿着 z 轴测量像素。然而,我们就是这样。你提供了一个长度,围绕该值构建深度的幻觉。

较小的数字会创建更极端的透视效果,就像你靠近元素一样。较大的数字会创建更温和的透视效果,就像通过远处的变焦镜头查看元素一样。非常高的透视值会创建等距效果,看起来和没有透视效果一样。

这是有一定道理的。你可以将透视视为一个金字塔,其顶点位于透视原点(默认为未变换元素位置的中心),其底部为你正在查看的浏览器窗口。顶点与底部之间的较短距离将创建一个较浅的金字塔,因此会产生更极端的扭曲。这在图 17-20 中有所说明,其中展示了代表 200 像素、800 像素和 2000 像素透视距离的假设金字塔。

css5 1720

图 17-20. 不同视角的金字塔

苹果的 Safari 文档中写道,透视值在300px以下会产生极端扭曲,2000px以上会产生“非常轻微”的扭曲,而500px1000px之间会产生“适度的透视”。图 17-21 展示了相同旋转的一系列元素,不同的透视值下展示的效果。

css5 1721

图 17-21. 不同透视值的效果

透视值必须始终为正数,非零长度。任何其他值都会导致perspective()函数被忽略。还要注意其在函数列表中的位置。如果你查看图 17-21 的代码,perspective()函数出现在rotateY()函数之前:

#ex1 {transform: perspective(100px) rotateY(-45deg);}
#ex2 {transform: perspective(250px) rotateY(-45deg);}
#ex3 {transform: perspective(500px) rotateY(-45deg);}
#ex4 {transform: perspective(1250px) rotateY(-45deg);}

如果你反转顺序,旋转会在应用透视之前发生,所以图 17-21 中的四个示例看起来完全相同。因此,如果你计划通过变换函数列表应用透视值,请确保它首先出现,或者至少出现在依赖它的任何变换之前。这提醒我们,编写transform函数的顺序非常重要。

更多变换属性

除了基本的transform属性和像rotate这样的独立变换属性外,还有一些相关的属性帮助定义元素变换原点、用于“场景”的透视等等。

移动变换的原点

到目前为止,我们所有的变换都有一个共同点:我们使用了元素的精确中心作为变换原点。例如,当旋转元素时,它围绕其中心旋转,而不是围绕角落旋转。这是默认行为,但通过属性transform-origin,你可以改变它。

语法定义看起来确实晦涩和令人困惑,但实际上非常简单。使用 transform-origin,你需要提供两个或三个长度或关键词来定义变换应该围绕的点:首先是水平方向,然后是垂直方向,可选地是沿 z 轴的长度。对于水平和垂直轴,你可以使用简单的英文关键词如 topright,百分比、长度,或者关键词和百分比或长度值的组合。对于 z 轴,不能使用简单的英文关键词或百分比,但可以使用任何长度值。像素是迄今为止最常见的单位。

长度值被视为从元素的左上角开始的距离。因此,transform-origin: 5em 22px 将使变换原点位于元素左侧内部 5 em,并且距离元素顶部下方 22 像素处。同样地,transform-origin: 5em 22px -200px 将使其位于左侧内部 5 em,下方 22 像素处,并且在元素的未变换位置的背后 200 像素处。

百分比是相对于元素的边界框的对应轴和大小计算的,作为相对于元素左上角的偏移量。例如,transform-origin: 67% 40% 将使变换原点位于元素左侧的 67% 处,距离元素顶部的 40% 处。图 17-22 展示了一些起始点计算的例子。

css5 1722

图 17-22. 不同的起始点计算

好了,如果你改变起始点,会发生什么?用二维旋转最容易理解这一点。假设你将一个元素向右旋转 45 度。其最终位置将取决于其起始点。图 17-23 展示了几个变换起点的效果;在每种情况下,变换起点都用圆圈标记。

css5 1723

图 17-23. 使用不同的变换起点的旋转效果

起始点对于其他变换类型也很重要,例如倾斜和缩放。使用中心作为起始点缩小元素将导致各边等比例收缩,而使用右下角作为起始点缩小元素将导致元素向该角收缩。类似地,相对于中心倾斜元素将产生与相对于右上角倾斜相同的形状,但形状的放置位置将不同。一些示例显示在图 17-24 中;同样地,每个变换起点都用圆圈标记。

css5 1724

图 17-24. 使用不同的变换起点的倾斜和缩放效果

唯一不受变换原点改变影响的变换类型是平移。如果你用translate()或其类似的方法如translateX()translateY(),或translate属性来移动一个元素,不管变换原点在哪里,元素最终都会停留在同一个位置。如果你计划做的只是移动,设置变换原点是不相关的。但是如果除了平移还要做其他事情,变换原点就很重要了。明智地使用它。

选择变换的框

我们前面的部分是写成变换原点始终相对于外部边缘计算的形式,这确实是 HTML 的默认设置,但 SVG 中并非总是如此。你可以至少在理论上通过transform-box属性进行更改。

两个值与在 HTML 上样式化时直接相关:

border-box

使用元素的边框框(由外边界定义)作为变换的参考框。

content-box

使用元素的内容框作为变换的参考框。

剩下的三个是为 SVG 目的设计的,尽管它们也可以应用于 HTML 上下文:

fill-box

使用元素的对象边界框作为变换的参考框。

stroke-box

使用元素的描边边界框作为变换的参考框。

view-box

使用元素的最近 SVG 视口作为参考框。

在 SVG 上下文中使用fill-box会导致对相应元素进行变换,正如我们从 HTML 中所期望的那样。另一方面,默认的view-box会导致所有变换都相对于由 SVG 的viewBox属性建立的坐标系统的原点进行计算。这种差异在图 17-25 中有所说明,该图是以下 SVG 文件及其包含的 CSS 的结果:

<svg xmlns="http://www.w3.org/2000/svg"
     width="500" height="200"
     fill="none" stroke="#000">
  <defs>
    <style>
      g rect {transform-origin: 0 0; transform: rotate(20deg);}
      g rect:nth-child(1) {transform-box: view-box;}
      g rect:nth-child(2) {transform-box: fill-box;}
    </style>
  </defs>
  <rect width="100%" height="100%" stroke-dasharray="4 3" />
  <rect x="100" y="50" width="100" height="100" />
  <rect x="300" y="50" width="100" height="100" />
  <g stroke-width="3" fill="#FFF8">
    <rect x="100" y="50" width="100" height="100" />
    <rect x="300" y="50" width="100" height="100" />
  </g>
</svg>

css5 1725

图 17-25. 围绕 SVG 原点和其自身原点旋转的方块

第一个方块位于左侧,从其起始点旋转 20 度,旋转中心为整个 SVG 文件的左上角(虚线框的左上角)。这是因为此方块的transform-box值为view-box。第二个方块的transform-boxfill-box,因此它使用自己的填充框的左上角——在 HTML 中我们称之为背景区域——作为旋转中心。

选择一个 3D 样式

如果你正在设置通过三维元素进行变换——比如使用translate3d()rotateY()——你可能期望元素呈现为在三维空间中的样子。transform-style属性有助于实现这一点。

假设你想把一个元素“靠近”你的眼睛,然后稍微倾斜一点,加入适度的透视效果。你可能会使用类似这样的规则:

div#inner {transform: perspective(750px) translateZ(60px) rotateX(45deg);}

<div id="outer">
outer
<div id="inner">inner</div>
</div>

所以,你这样做,得到的结果如图 17-26 所示——大体上是你可能期望的。

css5 1726

图 17-26. 一个 3D 转换的内部<div>

但是,当你决定将外部<div>旋转到一侧时,突然间一切都变得毫无意义了。内部<div>并不在你预想的位置。事实上,它看起来就像是粘贴在外部<div>前面的一幅图片。

好吧,这正是问题所在,因为transform-style的默认值是flat。内部div以其前移、后倾的状态绘制,这被应用到外部<div>的前面,就像一幅图片一样。因此,当你像图 17-27 中所示旋转外部<div>时,这幅扁平的图片也会随之旋转:

div#outer {transform: perspective(750px) rotateY(60deg) rotateX(-20deg);}
div#inner {transform: perspective(750px) translateZ(60px) rotateX(45deg);}

然而,将值更改为preserve-3d,结果会大不相同。内部div将作为完整的 3D 对象绘制,相对于其父级外部<div>在空间中浮动,而不是像粘贴在外部<div>前面的图片。你可以在图 17-27 中看到这种变化的结果:

div#outer {transform: perspective(750px) rotateY(60deg) rotateX(-20deg);
    transform-style: preserve-3d;}
div#inner {transform: perspective(750px) translateZ(60px) rotateX(45deg);}

css5 1727

图 17-27. 扁平与保留 3D 转换样式的效果

transform-style的一个重要方面是它可能会被其他属性覆盖。原因是这些其他属性的某些值要求以扁平化的方式呈现元素及其子元素才能工作。在这种情况下,transform-style的值被强制设为flat,无论你之前声明了什么。

因此,为了避免这种覆盖行为,请确保以下属性在任何具有 3D 转换子元素的 3D 转换容器元素上设置为列出的值:

  • overflow: visible

  • filter: none

  • clip: auto

  • clip-path: none

  • mask-image: none

  • mask-border-source: none

  • mix-blend-mode: normal

  • isolation: auto

这些都是这些属性的默认值,只要你不试图改变保留的 3D 元素的任何值,那么一切都没问题!但是,如果你发现编辑某些 CSS 突然使你可爱的 3D 变换变平了,其中一个属性可能是罪魁祸首。

改变透视

有两个属性用于定义透视的处理方式:一个用于定义透视距离,如前面章节中讨论的perspective()函数;另一个用于定义透视的原点。

定义一个透视组

首先,让我们考虑perspective属性,它接受一个长度来定义透视金字塔的深度。乍一看,它看起来就像是之前讨论的perspective()函数,但存在一些关键的差异。

举个快速的例子,如果你想创建一个非常深的透视效果,模仿使用变焦镜头的效果,你可以声明类似于perspective: 2500px。对于一个浅的透视深度,模仿特写镜头的效果,你可以声明perspective: 200px

那么这与perspective()函数有何不同呢?当你使用perspective()时,你为具有该规则的每个元素定义透视效果。因此,如果你写了transform: perspective(800px) rotateY(-50grad);,那么你将该透视应用于每个具有此规则的元素。

另一方面,使用perspective属性,你为接收该属性的元素的所有子元素创建了一个共享的透视。这里有一个区别的示例,如图 17-28 所示:

div {transform-style: preserve-3d; border: 1px solid gray; width: 660px;}
img {margin: 10px;}
#func {perspective: none;}
#func img {transform: perspective(800px) rotateX(-50grad);}
#prop {perspective: 800px;}
#prop img {transform: rotateX(-50grad);}

css5 1728

图 17-28。没有透视,独立的perspective(),以及共享的perspective

在图 17-28 中,我们首先看到一行未经转换的图像。在第二行中,每个图像都向我们旋转了 50 gradians(相当于 45 度),但每个图像都有自己独立的透视。

在第三行图像中,没有一个具有独立的透视。相反,它们都是在包含它们的<div>上设置了perspective: 800px;后所定义的共享透视中绘制的。由于它们都在共享的透视中运行,它们看起来“正确”——即,如果我们有三张物理图片安装在透明玻璃上,并绕其中心水平轴旋转向我们,我们会如预期那样看到它们。

这是perspective的关键区别,即属性和perspective()函数之间的差异。前者创建一个所有子元素共享的三维空间。后者仅影响应用它的元素。另一个区别是,perspective()函数的效果取决于在转换链中的调用顺序。perspective属性始终在所有其他转换之前应用,这通常是用来创建三维效果的理想方式。

在大多数情况下,你会使用perspective属性而不是perspective()函数。事实上,容器<div>(或其他元素)是三维变换的常见特征——它们以前用于页面布局——主要用于建立共享透视。在上面的例子中,<div id="two">完全是为了充当透视容器而存在。另一方面,如果没有它,我们就无法完成我们所做的事情。

移动透视的原点

当在三维空间中转换元素时,将使用透视效果。(请参见前面章节中的transform-styleperspective。)透视将有一个原点,也被称为消失点,你可以使用perspective-origin属性更改其位置。

使用perspective-origin,你定义了视线汇聚的点,与perspective类似,该点是相对于父容器定义的。

与大多数三维变换属性一样,这种效果比描述更容易展示。考虑以下 CSS 和标记示例,如图 17-29 所示:

#container {perspective: 850px; perspective-origin: 50% 0%;}
#ruler {height: 50px; background: #DED url(tick.gif) repeat-x;
    rotate: x 60deg;
    transform-origin: 50% 100%;}

<div id="container">
    <div id="ruler"></div>
</div>

css5 1729

Figure 17-29. 一个基本的“标尺”

我们有一个重复的背景图像,上面有一个标尺上的刻度,包含它们的 <div> 被向我们倾斜了 60 度。所有线条都指向一个共同的消失点,即容器 <div> 的顶部中心(由于 perspective-origin 的值是 50% 0%)。

现在考虑同样的设置,但使用不同的透视原点(图 17-30)。

css5 1730

Figure 17-30. 带有不同透视原点的基本“标尺”

如您所见,移动透视原点会改变 3D 变换元素的渲染效果。请注意,这些效果仅因为我们为 perspective 提供了一个值才能生效。如果 perspective 的值是默认的 none,则 perspective-origin 给定的任何值都将被忽略。这是有道理的,因为当没有透视时,就不可能有透视原点!

处理背面

多年来,你一直在布局元素,可能从未想过:“如果我们能看到元素的背面会是什么样子?”有了 3D 变换,如果有一天你真的看到了元素的背面,CSS 已经为你准备好了。发生的情况取决于 backface-visibility 属性。

与我们已经讨论过的许多其他属性和功能不同,这个属性非常简单。它只是确定当元素面向观众时,是否渲染其背面。仅此而已。

所以,假设你翻转了两个元素,一个元素的 backface-visibility 设置为默认值 visible,另一个设置为 hidden。你将得到如 图 17-31 所示的结果:

span {border: 1px solid red; display: inline-block;}
img {vertical-align: bottom;}
img.flip {rotate: x 180deg; display: inline-block;}
img#show {backface-visibility: visible;}
img#hide {backface-visibility: hidden;}

<span><img src="salmon.gif" alt="salmon"></span>
<span><img src="salmon.gif" class="flip" id="show" alt="salmon"></span>
<span><img src="salmon.gif" class="flip" id="hide" alt="salmon"></span>

css5 1731

Figure 17-31. 可见和隐藏的背面

如您所见,第一幅图像保持不变。第二幅图像沿其 x 轴翻转,因此我们从背面看到它。第三幅图像也已经翻转,但因为其背面已被隐藏,所以我们根本看不见它。

这个属性在几种情况下都很有用。在最简单的情况下,您有两个表示 UI 元素两侧的元素翻转的情况——比如一个带有其背面设置的搜索区域和偏好设置,或者一张照片的背面有一些信息。让我们考虑后一种情况。CSS 和标记可能看起来像这样:

section {position: relative;}
img, div {position: absolute; top: 0; left: 0; backface-visibility: hidden;}
div {rotate: y 180deg;}
section:hover {rotate: y 180deg; transform-style: preserve-3d;}

<section>
    <img src="photo.jpg" alt="">
    <div class="info">(…info goes here…)</div>
</section>

(如果有一个动画旋转,使卡片在 3D 空间中翻转,这将会更加有趣。)

此示例的变体使用相同的标记,但稍微不同的 CSS 来显示图像翻转后的背面。这可能更符合预期,因为它使信息看起来就像真的写在图像的背面一样。这导致了 图 17-32 所示的最终结果:

section {position: relative;}
img, div {position: absolute; top: 0; left: 0;}
div {rotate: y 180deg; backface-visibility: hidden;
    background: rgba(255,255,255,0.85);}
section:hover {rotate: y 180deg; transform-style: preserve-3d;}

css5 1732

Figure 17-32. 正面是照片,背面是信息

我们要做的一切只是将 backface-visibility: hidden<img><div> 都应用到 <div> 上。因此,当 <div> 翻转时,其反面被隐藏,但图像的反面则不是(嗯,还有使用半透明背景,这样我们可以看到文本和翻转后的图像)。

摘要

具有在二维和三维空间中转换元素的能力,CSS 变换为设计师提供了强大的功能。从创建有趣的二维变换组合,到创建完全仿三维的界面,变换在设计空间中开辟了大量新领域。一些属性之间存在依赖关系,这对于并非每个 CSS 作者一开始就能自然理解的事情,但通过实践后变得轻车熟路。

作者们经常使用变换来进行动画处理,例如卡片翻转、元素平滑缩放和旋转等。在接下来的两章中,我们将详细讨论这些过渡和动画是如何定义的。

第十八章:过渡

CSS 过渡允许我们随着时间从原始值动画 CSS 属性到新值。这些变化过渡了元素从一个状态到另一个状态,响应某种变化。通常这涉及用户交互,但也可能是由于脚本更改类、ID 或其他状态。

通常情况下,当 CSS 属性值发生变化时——即发生样式更改事件时——变化是瞬时的。新的属性值在重新绘制页面时(或在必要时重新布局和重绘)立即替换旧的属性值。大多数值的变化似乎是瞬时的,渲染时间少于 16 毫秒。即使变化时间超过这个时间(比如当一个大图像被未预取的图像替换时——这不是过渡,只是性能差),仍然是从一个值到另一个值的单一步骤。例如,当鼠标悬停在背景颜色上时,背景立即从一种颜色变为另一种颜色,没有渐变过程。

CSS 过渡

CSS 过渡提供了一种控制属性如何在一段时间内从一个值变化到另一个值的方法。因此,我们可以使属性值逐渐变化,创造(希望是)愉悦且不显眼的效果。

button {color: magenta;
    transition: color 200ms ease-in 50ms;
}
button:hover {color: rebeccapurple;
    transition: color 200ms ease-out 50ms;
}

在这个例子中,而不是在悬停时立即更改按钮的color值,transition属性意味着按钮的color将在 200 毫秒内从magenta逐渐淡入到rebeccapurple,甚至在开始过渡前添加 50 毫秒的延迟。

如果浏览器不支持 CSS 过渡属性(这种情况几乎不会发生),则变化是立即的而不是逐渐的,这完全没问题。如果某个属性或某些属性值不可动画化,则变化也将是即时的而不是逐渐的。

注意

当我们说可动画时,我们指的是可以通过过渡或动画(下一章的主题,第十九章)进行动画化的任何属性。本书中的属性定义框指出给定属性是否可动画。

通常您会希望即时进行值的更改。例如,链接颜色通常在悬停或焦点时立即更改,通知视觉用户发生了交互,并且焦点内容是一个链接。类似地,自动完成列表框中的选项不应该淡入:您希望选项立即出现,而不是在用户键入时比慢慢淡入。即时值的更改通常是最佳的用户体验。

在其他时候,您可能希望属性的值更渐变地变化,以引起注意。例如,您可能希望通过花费 200 毫秒来动画翻转卡片,使得卡片游戏更加逼真,因为如果没有动画,用户可能不会意识到发生了什么。

提示

寻找播放符号 以了解在线示例的可用性。本章中的所有示例都可以在 https://meyerweb.github.io/csstdg5figs/18-transitions 找到。

举个例子,您可能希望一些下拉菜单在 200 毫秒内展开或可见(而不是立即,这可能会令人不适)。使用过渡效果,您可以使下拉菜单缓慢显示。在 图 18-1 中,我们通过缩放变换来过渡子菜单的高度。这是 CSS 过渡的常见用法,我们稍后在本章中还将探讨这个话题。

警告

特别是快速过渡效果,尤其是移动距离较大或占据页面主要部分的过渡效果,可能会导致某些用户癫痫发作。为了降低或消除这种风险,请使用 prefers-reduced-motion 媒体查询(请参见 第二十一章)。始终牢记这些问题,并确保设计对癫痫和其他癫痫症患者具有可访问性。

css5 1801

图 18-1. 初始过渡,中间过渡和最终状态

过渡属性

在 CSS 中,过渡效果通过四个过渡属性来定义:transition-propertytransition-durationtransition-timing-functiontransition-delay,同时使用 transition 属性作为这些属性的缩写形式。

为了创建 图 18-1 中的下拉导航,我们使用了所有四个 CSS 过渡属性,以及一些定义过渡开始和结束状态的变换属性。以下代码定义了该示例的过渡效果:

nav li ul {
    transition-property: transform;
    transition-duration: 200ms;
    transition-timing-function: ease-in;
    transition-delay: 50ms;
    transform: scale(1, 0);
    transform-origin: top center;
}
nav li:is(:hover, :focus) ul {
    transform: scale(1, 1);
}

虽然我们在此示例中使用 :hover:focus 状态进行样式更改事件,但您也可以在其他情境下过渡属性。例如,您可以添加或删除类,或以其他方式改变状态—比如,从 :invalid 变为 :valid 或从 :checked 变为 :not(:checked)。或者您可以在斑马条纹表或基于 :nth-last-of-type 选择器的列表末尾附加表行或列表项。

在 图 18-1 中,嵌套列表的初始状态是 transform: scale(1, 0),并且使用 transform-origin: top center。最终状态是 transform: scale(1, 1),而 transform-origin 保持不变。(有关变换属性的更多信息,请参见 第十七章。)

在这个例子中,过渡属性定义了对transform属性的过渡:当在hover状态下设置新的transform值时,嵌套的无序列表将缩放到其原始默认大小,旧的transform: scale(1, 0)值和新的transform: scale(1, 1)值之间会平滑地变化,整个过程持续 200 毫秒。这个过渡在延迟 50 毫秒后开始,并缓慢地进入,这意味着它一开始会缓慢进行,然后随着时间的推移加快速度。

每当一个可动画目标属性发生变化时,如果该属性上设置了过渡效果,浏览器将应用过渡效果使变化逐渐进行。

请注意,所有过渡属性都是为默认的未悬停/未聚焦状态的<ul>元素设置的。这些状态仅用于改变变换,而不是过渡。这样做有一个很好的理由:这意味着菜单不仅在状态变化时会滑动打开,而且在悬停或焦点状态结束时也会滑动关闭。

想象一下如果过渡属性被应用于互动状态,就像这样:

nav li ul {
    transform: scale(1, 0);
    transform-origin: top center;
}
nav li:is(:hover, :focus) ul {
    transition-property: transform;
    transition-duration: 200ms;
    transition-timing-function: ease-in;
    transition-delay: 50ms;
    transform: scale(1, 1);
}

这意味着当未悬停聚焦时,元素将具有默认的过渡值——也就是说,没有过渡或瞬时过渡。我们之前示例中的菜单会在互动状态结束时滑动打开,但一旦互动状态结束,即不再处于互动状态时,过渡属性将不再适用!

也许你确实想要这种效果:平滑地打开但瞬间消失。如果是这样,请按照前面的示例应用过渡效果。否则,直接将它们应用于默认状态的元素,以便在进入和退出互动状态时应用过渡效果。当退出状态变化时,过渡时间会反转。你可以通过在初始和改变状态中声明不同的过渡来覆盖这种默认的反向过渡效果。

初始状态指的是在页面加载时匹配元素的状态。这可能意味着一个可编辑内容的元素可能会获得:focus,如下所示:

/* selector that matches elements `all` the time */
p[contenteditable] {
    background-color: background-color: rgb(0 0 0 / 0);
}
/* selector that matches elements `some` of the time */
p[contenteditable]:focus {
    /* overriding declaration */
    background-color: background-color: rgb(0 0 0 / 0.1);
}

在这个例子中,完全透明的背景始终是初始状态,只有在用户给元素焦点时才会改变。这就是我们在本章中提到的初始默认值的含义。选择器中包含的过渡属性将影响该元素在状态改变时的表现,包括从初始状态到改变状态(例如在上述例子中被聚焦时)。

初始状态也可以是一个临时状态,可能会改变,例如:checked复选框或:valid表单控件,甚至是一个被切换打开和关闭的类:

/* selector that matches elements `some` of the time */
input:valid {
    border-color: green;
}
/* selector that matches elements `some` of the time,
   when the prior selector does NOT match. */
input:invalid {
    border-color: red;
}
/* selector that matches elements `some` of the time,
   whether the input is valid or invalid */
input:focus {
    /* alternative declaration */
    border-color: yellow;
}

在此示例中,:valid:invalid选择器可以匹配任何给定元素,但永远不会同时匹配。如 图 18-2 中所示,:focus选择器在输入框有焦点时匹配,无论输入框是否同时匹配:valid:invalid选择器。

在这种情况下,当我们提到初始状态时,我们指的是原始值,可以是:valid:invalid。给定元素的更改状态是初始:valid:invalid状态的相反状态。

css5 1802

图 18-2. 在有效、无效和焦点状态下的输入框外观

记住,你可以将不同的过渡值应用于初始状态和更改后的状态,但始终要应用于进入给定状态时使用的值。以以下代码为例,这里设置了菜单在 2 秒内滑动打开,但在 200 毫秒内关闭:

nav li ul {
    transition-property: transform;
    transition-duration: 200ms;
    transition-timing-function: ease-in;
    transition-delay: 50ms;
    transform: scale(1, 0);
    transform-origin: top center;
}
nav li:is(:hover, :focus) ul {
    transition-property: transform;
    transition-duration: 2s;
    transition-timing-function: linear;
    transition-delay: 1s;
    transform: scale(1, 1);
}

这提供了一个糟糕的用户体验,但它阐明了这一点。 悬停或关注时,导航打开需要整整 2 秒。关闭时,它在 0.2 秒内迅速关闭。更改状态中的过渡属性在列表项悬停或关注时生效。因此,为这些状态定义的transition-duration: 2s生效。当菜单不再悬停或关注时,它返回到默认的缩小状态,并使用初始状态的过渡属性——nav li ul条件——导致菜单花费 200 毫秒关闭。

更仔细地查看示例,特别是默认的过渡样式。当用户停止悬停或关注父导航元素或子下拉菜单时,下拉菜单延迟 50 毫秒,然后开始200ms的过渡以关闭。这实际上是一种体验良好的用户体验样式,因为它给了用户一个(虽然很短暂的)机会将鼠标指针或焦点环回到菜单上,然后再开始关闭。

尽管可以单独声明四个过渡属性,但通常会使用简写。首先我们将逐个查看这四个属性,以便你能深入了解各自的作用。

通过属性限制过渡效果

transition-property属性指定要过渡的 CSS 属性的名称。这样可以限制只对某些属性进行过渡,而其他属性则立即改变。没错,说“transition-property属性”确实有点奇怪。

transition-property的值是属性的逗号分隔列表;如果您不想过渡任何属性,请使用关键字none;或默认的all,表示“过渡所有可动画属性”。您还可以在逗号分隔的属性列表中包含关键字all

如果将all作为唯一关键字包含——或默认为all——所有可动画属性将同时过渡。假设您想在悬停时改变框的外观:

div {
    color: #ff0000;
    border: 1px solid #00ff00;
    border-radius: 0;
    transform: scale(1) rotate(0deg);
    opacity: 1;
    box-shadow: 3px 3px rgb(0 0 0 / 0.1);
    width: 50px;
    padding: 100px;
}
div:hover {
    color: #000000;
    border: 5px dashed #000000;
    border-radius: 50%;
    transform: scale(2) rotate(-10deg);
    opacity: 0.5;
    box-shadow: -3px -3px rgb(255 0 0 / 0.5);
    width: 100px;
    padding: 20px;
}

当鼠标指针悬停在<div>上时,初始状态与悬停(更改)状态具有不同值的每个属性都将更改为悬停状态的值。transition-property属性用于定义哪些属性随时间动画(而不是立即更改)。所有属性都在hover时从默认值更改为悬停值,但只有在transition-property中包含的可动画属性会在过渡期间发生变化。像border-style之类的不可动画属性会立即从一个值更改为另一个值。

如果在transition-property的逗号分隔值中,all是唯一的值或者是最后一个值,那么所有可动画属性将同时过渡。否则,请提供一个逗号分隔的属性列表,以便过渡属性影响这些属性。

因此,如果我们希望过渡所有属性,下面的声明几乎是等效的:

div {
    color: #ff0000;
    border: 1px solid #00ff00;
    border-radius: 0;
    opacity: 1;
    width: 50px;
    padding: 100px;
    transition-property: color, border, border-radius, opacity,
        width, padding;
    transition-duration: 1s;
}
div {
    color: #ff0000;
    border: 1px solid #00ff00;
    border-radius: 0;
    opacity: 1;
    width: 50px;
    padding: 100px;
    transition-property: all;
    transition-duration: 1s;
}

两个transition-property属性声明都将过渡所有列出的属性,但前者仅过渡可能更改的六个属性。

在后一条规则中,transition-property: all确保了基于任何样式更改事件的可动画属性值的过渡时间为 1 秒。该过渡适用于选择器匹配的所有元素上应用的所有可动画属性,而不仅仅是在相同样式块中声明的属性。

在这种情况下,第一个版本将过渡限制为仅列出的六个属性,但使我们能够更好地控制每个属性的过渡方式。逐个声明属性使我们能够为每个属性的过渡提供不同的速度、延迟和/或持续时间:

div {
    color: #ff0000;
    border: 1px solid #0f0;
    border-radius: 0;
    opacity: 1;
    width: 50px;
    padding: 100px;
}
.foo {
    color: #00ff00;
    transition-property: color, border, border-radius, opacity,
        width, padding;
    transition-duration: 1s;
}
<div class="foo">Hello</div>

如果您想要分别为每个属性定义过渡效果,请将所有属性都写出来,并使用逗号分隔每个属性。如果您希望以相同的持续时间、延迟和速度动画几乎所有属性,但有少数例外,您可以同时使用all和您希望在不同时间、速度或速度下过渡的个别属性的组合。只需确保将all作为第一个值使用,因为在all之前列出的任何属性将包含在all中,覆盖您打算应用于这些现在被覆盖属性的任何其他过渡属性值:

div {
    color: #f00;
    border: 1px solid #00ff00;
    border-radius: 0;
    opacity: 1;
    width: 50px;
    padding: 100px;
    transition-property: all, border-radius, opacity;
    transition-duration: 1s, 2s, 3s;
}

逗号分隔值的all部分包括示例中列出的所有属性,以及所有继承的 CSS 属性,以及与元素匹配或继承的任何其他 CSS 规则块中定义的所有属性。

在上面的例子中,所有获取新值的属性将以相同的持续时间、延迟和时间函数过渡,除了border-radiusopacity,我们明确单独包含它们。因为我们将它们作为all后面逗号分隔列表的一部分包含,我们可以将它们与所有其他属性一起过渡,或者我们可以为这两个属性提供不同的时间、延迟和时间函数。在这种情况下,我们将所有属性过渡时间设置为 1 秒,除了border-radiusopacity,分别设置为 2 秒和 3 秒。(transition-duration属性将在后面的章节中讨论。)

通过属性限制抑制过渡

虽然默认情况下不会自动过渡时间,但如果您确实包含了 CSS 过渡,并且想要在特定场景中覆盖该过渡,您可以设置transition-property: none来覆盖整个过渡,并确保不会过渡任何属性。

none关键字只能作为属性的唯一值使用—不能将其作为属性的逗号分隔列表的一部分包含。如果您想要覆盖有限属性集的过渡,您必须列出仍然希望过渡的所有属性。您不能使用transition-property属性来排除属性;而是只能使用该属性来包含它们。

注意

另一种方法是将属性的延迟和持续时间设置为0s。这样,它将立即显示,就好像没有应用 CSS 过渡。

过渡事件

TransitionEvent 接口提供了四个与过渡相关的事件:transitionstarttransitionruntransitionendtransitioncancel。我们将重点关注transitionend,因为它是可以由单个 CSS 多次触发的事件。

每次过渡结束时,无论属性是单独声明还是作为all声明的一部分,都会触发一个transitionend事件,无论是向任何方向过渡还是在任何延迟后。一些看似简单的属性声明会使用多个transitionend事件,因为缩写属性中的每个可动画属性都会有自己的transitionend事件。考虑以下情况:

div {
    color: #f00;
    border: 1px solid #00ff00;
    border-radius: 0;
    opacity: 1;
    width: 50px;
    padding: 100px;
    transition-property: all, border-radius, opacity;
    transition-duration: 1s, 2s, 3s;
}

当过渡结束时,将会发生超过六个transitionend事件。例如,仅border-radius过渡就会产生四个transitionend事件,分别为以下每个:

  • border-bottom-left-radius

  • border-bottom-right-radius

  • border-top-right-radius

  • border-top-left-radius

padding 属性也是四个长手属性的缩写:

  • padding-top

  • padding-right

  • padding-bottom

  • padding-left

border 缩写属性产生八个 transitionend 事件:四个值用于由 border-width 缩写表示的四个属性,另外四个用于由 border-color 表示的属性:

  • border-left-width

  • border-right-width

  • border-top-width

  • border-bottom-width

  • border-top-color

  • border-left-color

  • border-right-color

  • border-bottom-color

border-style 属性没有 transitionend 事件,因为 border-style 不是一个可动画化的属性。

在列出了六个特定属性——colorborderborder-radiusopacitywidthpadding——的情况下,会有 19 个 transitionend 事件:这六个属性包括几个缩写属性的长手属性的每个值,以及可能从其他影响元素的继承或在其他样式块中声明的属性值。

你可以像这样监听 transitionend 事件:

document.querySelector("div").addEventListener("transitionend",
    , (e) => {
      console.log(e.propertyName);
});

transitionend 事件包含三个特定事件属性:

propertyName

CSS 属性刚刚完成过渡的名称。

pseudoElement

过渡发生在的伪元素,由两个分号引导,如果过渡是在常规 DOM 节点上,则为空字符串。

elapsedTime

过渡运行所花费的时间,通常是在 transition-duration 属性中列出的时间。

对于成功过渡到新值的每个属性,将会发生一个 transitionend 事件。如果过渡被中断,比如移除导致过渡的状态更改或同一元素上的另一个属性更改,则不会触发。尽管如此,当它恢复到初始值或完成由元素上的其他属性值更改所做的值的过渡时, 发生 transitionend 事件。

当属性返回到其初始值时,会再次触发 transitionend 事件。只要过渡开始,即使没有在原始方向完成初始过渡,此事件也会发生。

设置过渡持续时间

transition-duration 属性的值是以逗号分隔的时间长度列表,单位可以是秒(s)或毫秒(ms)。这些时间值描述了从一个状态过渡到另一个状态所需的时间。

在两个状态之间移动时,如果仅为其中一个状态声明了持续时间,则过渡持续时间将仅用于 该状态的过渡。考虑以下情况:

input {
    transition: background-color;
}
input:invalid {
    transition-duration: 1s;
    background-color: red;
}
input:valid {
    transition-duration: 0.2s;
    background-color: green;
}

因此,当输入无效时,将需要 1 秒才能将其更改为红色背景,当其有效时,仅需 200 毫秒即可过渡为绿色背景。

transition-duration 属性的值为正数,可以是秒 (s) 或毫秒 (ms)。规范要求使用 mss 的时间单位,即使持续时间设置为 0s。默认情况下,属性从一个值瞬间变为另一个值,不显示可见动画,这就是过渡持续时间的默认值为 0s 的原因。

除非在属性上设置了 transition-delay 的正值,否则如果省略了 transition-duration,则好像未应用 transition-property 声明一样,并且不会发生 transitionend 事件。只要过渡的总持续时间大于 0 秒(即 transition-duration 大于 transition-delay,包括大于默认的 0s 延迟),过渡仍将被应用,并且在过渡完成时会发生 transitionend 事件。

负值对于 transition-duration 是无效的,如果包含负值,则将使整个 transition-duration 声明无效。

使用前面相同冗长的 transition-property 声明,我们可以为所有属性声明单一持续时间,为每个属性声明单独的持续时间,或者我们可以使交替的属性在相同的时间内动画化。我们可以通过包含单一的 transition-duration 值来为过渡期间的所有属性声明单一持续时间:

 div {
    color: #ff0000;
    …
    transition-property: color, border, border-radius, opacity,
        width, padding;
    transition-duration: 200ms;
}

我们还可以为 transition-duration 属性值声明与 transition-property 属性值中列出的 CSS 属性相同数量的逗号分隔的时间值。如果我们希望每个属性过渡的时间不同,我们必须为每个声明的属性名称包含一个不同的逗号分隔值:

div {
    color: #ff0000;
    …
    transition-property: color, border, border-radius, opacity,
        width, padding;
    transition-duration: 200ms, 180ms, 160ms, 120ms, 1s, 2s;
}

如果声明的属性数量与声明的持续时间数量不匹配,则浏览器有特定的规则来处理不匹配情况。如果持续时间多于属性数量,则多余的持续时间将被忽略。如果属性数量多于持续时间,则持续时间将重复。在以下示例中,colorborder-radiuswidth 的持续时间为 100 毫秒;borderopacitypadding 将设置为 200 毫秒。

div {
    …
    transition-property: color, border, border-radius, opacity,
        width, padding;
    transition-duration: 100ms, 200ms;
}

如果我们声明了正好两个逗号分隔的持续时间,那么每个奇数属性将在第一次声明的时间内进行过渡,每个偶数属性将在第二次声明的时间内进行过渡。

提示

记住用户体验很重要。如果过渡太慢,网站会显得缓慢或不响应,吸引了本不应该引起的注意力。如果过渡太快,可能会太微妙而不易察觉。视觉效果应该足够长以便被看到,但不应太长以至于成为注意的中心。通常,一个可见但不分散注意力的过渡的最佳持续时间是 100 到 300 毫秒。

调整过渡的内部时间

想要过渡开始时缓慢并加速,开始时快速并结束缓慢,一直以相同速度前进,跳过多个步骤,甚至反弹吗?transition-timing-function 提供了控制过渡速度的方法。

transition-timing-function 值包括 easelinearease-inease-outease-in-outstep-startstep-endsteps(n, start)—其中 n 是步骤数—steps(n, end),以及 cubic-bezier(x1, y1, x2, y2)。(这些值也是 animation-timing-function 的有效值,并在第 19 章中有详细描述。)

立方贝塞尔时间函数

非步进关键字是作为平滑曲线提供的缓和时间函数,它们是对提供平滑曲线的数学贝塞尔函数的别名。规范中提供了五个预定义的缓和函数,如表 18-1 所示。

表 18-1. 支持的立方贝塞尔时间函数关键字

时间函数 描述 Cubic Bézier 值
cubic-bezier() 指定一个立方贝塞尔曲线 cubic-bezier(*x1, y1, x2, y2*)
ease 起始缓慢,然后加速,之后减速,最后非常缓慢结束 cubic-bezier(0.25, 0.1, 0.25, 1)
linear 在整个过渡期间保持相同速度 cubic-bezier(0, 0, 1, 1)
ease-in 开始缓慢,然后加速 cubic-bezier(0.42, 0, 1, 1)
ease-out 开始快速,然后减速 cubic-bezier(0, 0, 0.58, 1)
ease-in-out 类似于 ease;中间快速,起始缓慢但结束时不像那么慢 cubic-bezier(0.42, 0, 0.58, 1)

立方贝塞尔曲线,包括在表 18-1 中显示的五个命名缓和函数的底层曲线,以及图 18-3 中显示的曲线,需要四个数值参数。例如,linear 相当于 cubic-bezier(0, 0, 1, 1)。第一个和第三个立方贝塞尔函数的参数值需要在 0 和 1 之间。

css5 1803

图 18-3. 命名立方贝塞尔函数的曲线表示

cubic-bezier() 函数中的四个数字定义了一个盒子内两个控制点xy坐标。这些控制点是从盒子的左下角到右上角延伸的线段的端点。曲线是通过贝塞尔函数使用两个角和两个控制点的坐标来构建的。

要了解其工作原理,请查看图 18-4 中显示的曲线及其对应的值。

css5 1804

图 18-4. 四个贝塞尔曲线及其cubic-bezier()值(来自http://cubic-bezier.com

考虑第一个例子。前两个值对应x1y1,分别是0.51。如果你在方框的中间横跨一半(x1 = 0.5),并且向方框顶部走到底(y1 = 1),你就到了第一个控制点的位置。同样,x2,y2的坐标0.5,0描述了方框中心底部的点,即第二个控制点的位置。所示的曲线是由这些控制点的放置产生的。

在第二个例子中,控制点的位置被交换,曲线也随之改变。第三和第四个例子也是彼此倒置的。注意在交换控制点位置时,得到的曲线有何不同。

预定义的关键术语相当有限。为了更好地遵循动画原则,您可能希望使用包含四个浮点值的立方贝塞尔函数,而不是预定义的关键字。如果你精通微积分或者在像 Illustrator 这样的程序上有很多经验,你也许能够自己脑补立方贝塞尔函数;否则,在线工具可以让你尝试不同的值,比如http://cubic-bezier.com,这个网站可以让你比较常见的关键字或者你自己的立方贝塞尔函数。

如图 18-5 所示,网站http://easings.net提供了许多额外的立方贝塞尔函数值,供您使用以提供更逼真、令人愉悦的动画效果。

css5 1805

图 18-5. 有用的作者定义的立方贝塞尔函数(来自http://easings.net

网站的作者为他们的动画命名,但这些名称不属于 CSS 规范的一部分,必须按照表 18-2 中显示的方式书写。

表 18-2. 立方贝塞尔定时

非官方名称 立方贝塞尔函数值
easeInSine cubic-bezier(0.47, 0, 0.745, 0.715)
easeOutSine cubic-bezier(0.39, 0.575, 0.565, 1)
easeInOutSine cubic-bezier(0.445, 0.05, 0.55, 0.95)
easeInQuad cubic-bezier(0.55, 0.085, 0.68, 0.53)
easeOutQuad cubic-bezier(0.25, 0.46, 0.45, 0.94)
easeInOutQuad cubic-bezier(0.455, 0.03, 0.515, 0.955)
easeInCubic cubic-bezier(0.55, 0.055, 0.675, 0.19)
easeOutCubic cubic-bezier(0.215, 0.61, 0.355, 1)
easeInOutCubic cubic-bezier(0.645, 0.045, 0.355, 1)
easeInQuart cubic-bezier(0.895, 0.03, 0.685, 0.22)
easeOutQuart cubic-bezier(0.165, 0.84, 0.44, 1)
easeInOutQuart cubic-bezier(0.77, 0, 0.175, 1)
easeInQuint cubic-bezier(0.755, 0.05, 0.855, 0.06)
easeOutQuint cubic-bezier(0.23, 1, 0.32, 1)
easeInOutQuint cubic-bezier(0.86, 0, 0.07, 1)
easeInExpo cubic-bezier(0.95, 0.05, 0.795, 0.035)
easeOutExpo cubic-bezier(0.19, 1, 0.22, 1)
easeInOutExpo cubic-bezier(1, 0, 0, 1)
easeInCirc cubic-bezier(0.6, 0.04, 0.98, 0.335)
easeOutCirc cubic-bezier(0.075, 0.82, 0.165, 1)
easeInOutCirc cubic-bezier(0.785, 0.135, 0.15, 0.86)
easeInBack cubic-bezier(0.6, -0.28, 0.735, 0.045)
easeOutBack cubic-bezier(0.175, 0.885, 0.32, 1.275)
easeInOutBack cubic-bezier(0.68, -0.55, 0.265, 1.55)

步骤时间

步骤时间函数也是可用的,以及四个预定义的步骤值;参见表 18-3。

表 18-3. 步骤时间函数

时间函数 定义
steps(<integer>, jump-start) 显示<integer>关键帧,在过渡开始时的最后n/100%持续时间内显示最后一个关键帧;第一次跳跃发生在过渡的最开始。start可以替代jump-start
steps(<integer>, jump-end) 显示<integer>关键帧,在过渡持续时间的开始n/100%内保持初始状态;最后一次跳跃发生在过渡的最后。end可以替代jump-end
steps(<integer>, jump-both) 显示<integer>关键帧,从立即跳跃开始,直到过渡持续时间结束时的最后一次跳跃;这实际上增加了一个步骤到过渡中
steps(<integer>, jump-none) 显示<integer>关键帧,但在过渡持续时间的开始或结束时没有跳跃,而是在前n/100%的时间内保持初始值,并在最后n/100%的时间内显示最终值;这实际上从过渡中移除了一个步骤
step-start 在整个过渡持续时间内停留在最后一个关键帧;等同于steps(1, jump-start)
step-end 在整个过渡持续时间内停留在初始关键帧;等同于steps(1, jump-end)

如图 18-6 所示,步骤时间函数显示了过渡从初始值到最终值的步骤进展,而不是平滑曲线。

css5 1806

图 18-6. 步骤时间函数

步骤时间函数允许您通过定义步数和步骤的方向来划分过渡成等距的步骤。

使用jump-start时,第一步发生在动画或过渡开始时。使用jump-end时,最后一步发生在动画或过渡结束时。例如,steps(5, jump-end)会在 0%、20%、40%、60%和 80%的等距步骤中跳跃;而steps(5, jump-start)会在 20%、40%、60%、80%和 100%的等距步骤中跳跃。

step-start 函数与 steps(1, jump-start) 相同。当使用时,过渡属性值从开始到结束都保持在它们的最终值上。step-end 函数与 steps(1, jump-end) 相同,将过渡值设置为它们的初始值,并在整个过渡期间保持在那里。

注意

步进时间函数,特别是 jump-startjump-end 的确切含义,在 第十九章 中有深入讨论。

继续使用我们之前使用过的同样冗长的 transition-property 声明,我们可以为所有属性声明单个时间函数,或为每个属性定义单独的时间函数等。在这里,我们将所有过渡属性设置为单一的持续时间和时间函数:

div {
    transition-property: color, border-width, border-color, border-radius,
        opacity, width, padding;
    transition-duration: 200ms;
    transition-timing-function: ease-in;
}

永远记住,transition-timing-function 不会改变过渡属性所需的时间:这是由 transition-duration 属性设置的。它只是改变了在设定时间内过渡的进度。请考虑以下内容:

div {
    …
    transition-property: color, border-width, border-color, border-radius,
        opacity, width, padding;
    transition-duration: 200ms;
    transition-timing-function: ease-in, ease-out, ease-in-out,
        step-end, step-start, steps(5, jump-start), steps(3, jump-end);
}

如果我们为这七个属性设置了这七个时间函数,并且它们具有相同的过渡持续时间和延迟,所有属性都会在相同的时间开始和结束过渡。(顺便说一句,前面的过渡体验真是糟糕透顶,请不要这样做。)

熟悉各种时间函数的最佳方法是尝试它们,并看看哪一个对您想要的效果最有效。在测试时,设置一个相对较长的 transition-duration 可以更好地可视化各种函数之间的差异。在更高的速度下,您可能无法区分不同的缓动函数。只是不要忘记在发布结果之前将过渡速度恢复为更快的速度!

延迟过渡

transition-delay 属性允许您在应用于元素的变化开始引发过渡和实际过渡开始之间引入延迟。

transition-delay 的值为 0s(默认值)意味着过渡会立即开始;当元素的状态改变时立即执行。例如,这在 a:hover 的瞬间变化效果中很常见。

transition-delay 的值不为 0s 时,transition-delay 的 <time> 值定义了从属性值通常会改变的时刻到 transitiontransition-property 值声明的属性值开始动画到最终值之间的时间偏移量。

有趣的是,时间的负值是有效的。您可以使用负的 transition-delay 创建的效果在 “负延迟值” 中有描述。

继续使用我们已经使用过的包含 6 个(或 19 个)属性的 transition-property 声明,我们可以通过省略 transition-delay 属性或将其值设置为 0s,使所有属性立即开始过渡。另一种可能性是,我们可以让一半的过渡立即开始,然后另一半延迟 200 毫秒,如下所示:

div {
    transition-property: color, border, border-radius, opacity,
        width, padding;
    transition-duration: 200ms;
    transition-timing-function: linear;
    transition-delay: 0s, 200ms;
}

通过在一系列属性上包含 transition-delay: 0s, 200ms,每个属性的过渡时间为 200 毫秒,我们使得 colorborder-radiuswidth 立即开始过渡。其余属性将在其他过渡完成后开始过渡,因为它们的 transition-delay 等于应用于所有属性的 transition-duration

transition-durationtransition-timing-function 一样,当逗号分隔的 transition-delay 值超过逗号分隔的 transition-property 值时,多余的延迟值将被忽略。当逗号分隔的 transition-property 值超过逗号分隔的 transition-delay 值时,延迟值将被重复。

我们甚至可以声明七个 transition-delay 值,以便每个属性在前一个属性过渡后开始过渡,如下所示:

div {
    …
    transition-property: color, border-width, border-color, border-radius,
        opacity, width, padding;
    transition-duration: 200ms;
    transition-timing-function: linear;
    transition-delay: 0s, 0.2s, 0.4s, 0.6s, 0.8s, 1s, 1.2s;
}

在这个例子中,我们声明每个过渡的持续时间为 200 毫秒,使用 transition-duration 属性。然后,我们声明了一个 transition-delay,为每个属性提供了逗号分隔的延迟值,每个属性的延迟值递增 200 毫秒,即 0.2 秒——与每个属性过渡的持续时间相同。最终结果是,每个属性在前一个属性完成过渡后开始过渡。

我们可以利用数学方法,为每个过渡属性指定不同的持续时间和延迟时间,确保它们都在相同的时间完成过渡:

div {
    …
    transition-property: color, border-width, border-color, border-radius,
        opacity, width, padding;
    transition-duration: 1.4s, 1.2s, 1s, 0.8s, 0.6s, 0.4s, 0.2s;
    transition-timing-function: linear;
    transition-delay: 0s, 0.2s, 0.4s, 0.6s, 0.8s, 1s, 1.2s;
}

在这个例子中,每个属性在 1.4 秒标记处完成过渡,但每个属性的持续时间和延迟时间不同。对于每个属性,transition-duration 值加上 transition-delay 值总共达到 1.4 秒。

通常情况下,您希望所有的过渡同时开始。您可以通过包含单个 transition-delay 值来实现这一点,该值应用于所有属性。在我们的下拉菜单中,例如 图 18-1,我们包含了一个 50 毫秒的延迟。这种延迟时间不够长,以至于用户察觉不到,也不会导致应用程序显得缓慢。相反,50 毫秒的延迟可以帮助防止用户在将鼠标从页面或应用程序的一个部分快速移动到另一个部分时,意外悬停在菜单项上,从而导致导航意外打开。

负延迟值

transition-delay为负且小于transition-duration的值将导致过渡立即开始,在过渡的中间部分。例如:

div {
  transform: translateX(0);
  transition-property: transform;
  transition-duration: 200ms;
  transition-delay: -150ms;
  transition-timing-function: linear;
}
div:hover {
  transform: translateX(200px);
}

给定transition200mstransition-delay-150ms,过渡将在过渡的四分之三处开始,并持续 50 毫秒。在这种情况下,给定线性时间函数,<div>在悬停时立即跳转到沿 x 轴移动150px,然后在 50 毫秒内从 150 像素动画移动到 200 像素。

如果负的transition-delay的绝对值大于或等于transition-duration,则属性值的更改是立即的,就像未应用transition一样,并且不会发生transitionend事件。

在从悬停状态过渡回原始状态时,默认情况下,应用相同值的transition-delay。在前述场景中,由于在悬停状态下未覆盖transition-delay,当用户停止悬停在元素上时,<div>将跳转到沿 x 轴移动 50 像素,然后花费 50 毫秒返回到其初始位置,即沿 x 轴移动 0 像素。

使用过渡简写

transition属性将我们到目前为止讨论的四个属性——transition-propertytransition-durationtransition-timing-functiontransition-delay——合并为一个简写属性。

transition属性接受值none,或逗号分隔的一组单个过渡。单个过渡包含要过渡的单个属性,或关键字all以过渡所有属性;过渡的持续时间;时间函数;和过渡延迟。

如果在transition简写中,单个过渡省略了要过渡的属性,则该单个过渡将默认为all。如果省略了transition-timing-function值,则默认为ease。如果只包括一个时间值,则将作为持续时间,并且不会发生延迟,就像transition-delay被设置为0s一样。

在每个单独的过渡中,持续时间与延迟的顺序很重要:第一个可以解析为时间的值将被设置为持续时间。如果在逗号或语句结束之前找到额外的时间值,则将其设置为延迟。

以下是写入相同过渡效果的三种等效方式:

nav li ul {
    transition: transform 200ms ease-in 50ms,
                  opacity 200ms ease-in 50ms;
}
nav li ul {
    transition: all 200ms ease-in 50ms;
}
nav li ul {
    transition: 200ms ease-in 50ms;
}

在第一个示例中,我们看到了表达正在过渡的两个属性的简写方式。因为我们正在过渡所有将被改变的属性(在代码块中未显示的其他规则中),我们可以使用关键字 all,就像第二个示例中所示。而且,因为 all 是默认值,我们可以只写持续时间、时间函数和延迟的简写形式。如果我们使用 ease 而不是 ease-in,我们可以省略时间函数,因为 ease 是默认的。如果我们不想要延迟,我们可以省略第二个时间值,因为 0s 是默认值。

我们必须包括持续时间,否则过渡将不可见。换句话说,transition 属性值中唯一被认为是必需的部分是 transition-duration

如果我们只想延迟从关闭菜单到打开菜单的变化而没有渐变过渡,我们仍然需要包括 0s 的持续时间。请记住,第一个可解析为时间的值将被设置为持续时间,第二个值将被设置为延迟时间:

nav li ul {
  transition: 0s 200ms;
}
警告

此过渡将等待 200 毫秒,然后完全打开下拉并且不透明,没有渐变过渡。创建没有过渡的延迟是一个糟糕的用户体验,请不要这样做。

如果我们有一个用逗号分隔的过渡列表(而不仅仅是单个声明),并且包含了 none 这个词,整个过渡声明将是无效的并且将被忽略。您可以为四个长手过渡属性声明逗号分隔的值,或者可以包括多个简写过渡的逗号分隔列表:

div {
    transition-property: color, border-width, border-color, border-radius,
        opacity, width, padding;
    transition-duration: 200ms, 180ms, 160ms, 140ms, 100ms, 2s, 3s;
    transition-timing-function: ease, ease-in, ease-out, ease-in-out,
        step-end, steps(5, start), steps(3, end);
    transition-delay: 0s, 0.2s, 0.4s, 0.6s, 0.8s, 1s, 1.2s;
}
div {
    transition:
        color 200ms ease,
        border-width 180ms ease-in 200ms,
        border-color 160ms ease-out 400ms,
        border-radius 140ms ease-in-out 600ms,
        opacity 100ms step-end 0.8s,
        width 2s steps(5, start) 1s,
        padding 3s steps(3, end) 1.2s;
}

前两个 CSS 规则块在功能上是等效的。当将多个简写过渡字符串成过渡列表时,请小心使用:transition: color, opacity 200ms ease-in 50ms 将在 50 毫秒延迟后以 200 毫秒的持续时间缓慢渐变不透明度,但 color 的变化将是瞬间的,没有 transitionend 事件。它仍然有效,但可能不是您想要的效果。

反向中断的过渡

当过渡在能够完成之前被打断时(例如在下拉菜单完成打开过渡之前鼠标移出),属性值将被重置为过渡开始前的值,并且属性会过渡回这些值。因为在还原部分过渡上重复持续时间和时间函数可能导致奇怪或者糟糕的用户体验,CSS 过渡规范提供了缩短还原过渡的选项。

假设我们在默认状态的菜单上设置了 transition-delay50ms,并且在悬停状态下没有声明过渡属性;因此,浏览器将等待 50 毫秒才开始反向(或关闭)过渡。

当向前动画完成过渡到最终值并触发transitionend事件时,所有浏览器都会在反向状态下复制transition-delay。假设用户在菜单开始过渡后 75 毫秒移开。这意味着下拉菜单将在完全打开和完全不透明之前动画关闭。浏览器在关闭菜单之前应该有 50 毫秒的延迟,就像在打开菜单之前等待 50 毫秒一样。这实际上是良好的用户体验,因为它在关闭之前提供了几毫秒的延迟,防止用户意外地离开菜单时产生抖动行为。

在步进定时函数的情况下,如果过渡时间是 10 秒,分为 10 步,并且在 3.25 秒后恢复属性,结束时在第三步和第四步之间的四分之一处(完成三步,或过渡的 30%),则恢复到先前值将需要 3 秒钟。在以下示例中,我们的<div>宽度将在鼠标移出时从 100 像素增长到 130 像素,然后开始恢复:

div {
    width: 100px;
    transition: width 10s steps(10, jump-start);
}
div:hover {
    width: 200px;
}

尽管反向持续时间将被四舍五入为达到最近执行的步骤所需的时间,但反向方向将根据最初声明的步骤数而不是完成的步骤数来分割。在我们的 3.25 秒案例中,通过 10 步恢复将需要 3 秒钟。这些反向过渡步骤的持续时间为每个步骤 300 毫秒,每个步骤将宽度缩小 3 像素,而不是 10 像素。

如果定时函数是线性的,那么在两个方向上的持续时间将是相同的。所有其他cubic-bezier函数在被中断之前的初始过渡进度上的持续时间是成比例的。负的transition-delay值也会被相应缩短。正的延迟在两个方向上保持不变。

对于悬停状态,没有浏览器会有transitionend,因为过渡没有结束;但当菜单完成折叠时,所有浏览器都会在反向状态下触发transitionend事件。该反向过渡的elapsedTime取决于浏览器是否花费了完整的 200 毫秒来关闭菜单,或者像部分打开菜单那样花费了同样长的时间关闭菜单。

要覆盖这些值,请在初始和最终状态中包含过渡属性(例如,未悬停和悬停样式)。虽然这不会影响反向缩短,但它确实提供了更多控制。

警告

警惕同时在祖先和后代上进行过渡。例如,在一个元素上过渡继承属性后不久,在后代或祖先节点上过渡相同的属性可能会导致意外的结果。如果后代上的过渡在祖先上的过渡完成之前完成,后代将继续从其父级继承(仍在过渡中的)值。这种效果可能不是您期望的。

可动画化的属性和值

在实施过渡和动画之前,了解并非所有属性都可动画化是很重要的。您可以过渡(或动画化)任何可动画化的 CSS 属性;但哪些属性是可动画化的呢?

发展对哪些属性可以进行动画化的感觉的关键之一是识别哪些属性具有可以插值的值。插值是在已知数据点的值之间构造数据点。确定属性值是否可动画化的关键指南是计算值是否可以插值。如果属性的计算值是关键字,则不能插值;如果关键字计算为某种数字,则可以。一个快速的判断标准是,如果您可以确定两个属性值之间有一个中间点,那么这些属性值可能是可动画化的。

例如,像display值如blockinline-block不是数值的,因此没有中间点;它们不可动画化。transform属性值如rotate(10deg)rotate(20deg)有一个中间点rotate(15deg);它们可以动画化。

border属性是border-styleborder-widthborder-color的简写(这些又是四边值的简写属性)。虽然在border-style值之间没有中间点,但border-width属性的长度单位是数值,因此它们可以被动画化。mediumthickthin的关键字值有数值等效,可以插值:border-width属性的计算值将这些关键字转换为长度。

border-color值中,颜色是数值的——所有命名颜色都可以用十六进制或其他数值颜色表示——因此颜色也是可以动画化的。如果您从border: red solid 3px过渡到border: blue dashed 10px,边框宽度和边框颜色将以定义的速度过渡,但border-style将立即从solid跳转到dashed

在同样的思路下,接受数值作为参数的 CSS 函数通常是可以动画化的。有一个例外的规则是离散动画类型的属性,比如visibility:虽然visiblehidden之间没有中间值,但visibility的值在这些离散值之间跳跃,从可见到不可见。对于visibility属性,当初始值或目标值是visible时,值将在从visiblehidden的过渡结束时改变。对于从hiddenvisible的过渡,值将在过渡开始时改变。

auto值通常应被视为不可动画化,并应避免用于动画和过渡。根据规范,它不是一个可动画化的值,但一些浏览器将auto的当前数值(如height: auto)插值为0px或可能是fit-content()函数。对于像heightwidthtopbottomleftrightmargin这样的属性,auto值是不可动画化的。

通常可以使用替代属性或值。例如,不要将height: 0更改为height: auto,而是使用max-height: 0max-height: 100vh,这将通常产生预期效果。对于min-heightmin-widthauto值是可以动画化的,因为min-height: auto实际上计算为 0。

属性值的插值方式

数字作为浮点数进行插值。整数作为整数进行插值,因此作为整数递增或递减。

在 CSS 中,长度和百分比单位被转换为实数。当过渡或动画calc()函数时,从一种长度类型到百分比,值将被转换为calc()函数并插值为实数。

无论是 HSLA、RGB 还是像aliceblue这样的命名颜色,颜色都被转换为它们的 RGBA 等效值进行过渡,并在 RGBA 颜色空间中插值。如果要在不同的颜色空间(如 HSL)中进行插值,请确保过渡前后的颜色在同一个颜色空间中(在本例中为 HSL)。

在动画字体粗细时,如果使用像bold这样的关键字,它们将被转换为数值并进行动画化。

包含具有多个组件的可动画化属性值时,每个组件都会适当地进行插值。例如,text-shadow最多有四个组件:颜色、xyblur。颜色作为color插值,而xyblur组件作为长度插值。

盒阴影有两个额外的可选关键字:inset(或缺少)和spread。因为spread是一个长度,所以它可以插值。inset关键字无法转换为数值等效,因此没有办法在内嵌阴影和投影阴影之间逐渐过渡。

与具有多个组件的值类似,只有当您转换相同类型的梯度(线性、径向或圆锥)且具有相同数量的颜色停止时,才能转换渐变。然后,每个颜色停止的颜色作为颜色进行插值,每个颜色停止的位置作为长度和百分比单位进行插值。

插值重复值

当您有其他类型属性的简单列表时,列表中的每一项都会根据该类型适当地进行插值处理,只要列表具有相同数量的项目或可重复项目,并且每一对值都可以进行插值。例如:

.img {
    background-image:
        url(1.gif), url(2.gif), url(3.gif), url(4.gif),
        url(5.gif), url(6.gif), url(7.gif), url(8.gif),
        url(9.gif), url(10.gif), url(11.gif), url(12.gif);
    transition: background-size 1s ease-in 0s;
    background-size: 10px 10px, 20px 20px, 30px 30px, 40px 40px;
}
.img:hover {
    background-size: 25px 25px, 50px 50px, 75px 75px, 100px 100px;
}

在转换四个background-size,所有大小都以像素列出的情况下,来自预过渡状态的第三个background-size可以逐渐过渡到转换列表的第三个background-size。在上述示例中,当悬停时,背景图像 1、5 和 9 将从10px过渡到25px的高度和宽度。类似地,图像 3、7 和 11 将从30px过渡到75px,依此类推。

因此,background-size值会重复三次,就像 CSS 写成以下内容一样:

.img {
    …
    background-size: 10px 10px, 20px 20px, 30px 30px, 40px 40px,
                     10px 10px, 20px 20px, 30px 30px, 40px 40px,
                     10px 10px, 20px 20px, 30px 30px, 40px 40px;
    …
}
.img:hover {
    background-size: 25px 25px, 50px 50px, 75px 75px, 100px 100px,
                     25px 25px, 50px 50px, 75px 75px, 100px 100px,
                     25px 25px, 50px 50px, 75px 75px, 100px 100px;
}

如果属性的逗号分隔值不足以匹配背景图像的数量,则值列表将重复,直到足够为止,即使动画状态中的列表与初始状态不匹配:

.img:hover {
    background-size: 33px 33px, 66px 66px, 99px 99px;
}

如果我们从初始状态的四个background-size声明转换为动画状态的三个background-size声明,仍然以像素列出,并且具有 12 个背景图像,则动画和初始状态的值将重复(分别三次和四次),直到我们有 12 个必要的值,就好像已声明如下:

.img {
    …
    background-size: 10px 10px, 20px 20px, 30px 30px,
                     40px 40px, 10px 10px, 20px 20px,
                     30px 30px, 40px 40px, 10px 10px,
                     20px 20px, 30px 30px, 40px 40px;
    …
}
.img:hover {
    background-size: 33px 33px, 66px 66px, 99px 99px,
                     33px 33px, 66px 66px, 99px 99px,
                     33px 33px, 66px 66px, 99px 99px,
                     33px 33px, 66px 66px, 99px 99px;
}

如果一对值无法插值化,例如如果background-size从默认状态的contain更改为悬停时的cover,则根据规范,列表不可插值化。然而,一些浏览器会忽略用于过渡的特定值对,并仍会对可插值化的值进行动画化。

如果浏览器能推断出隐式值,则某些属性值可以动画化。例如,对于阴影,浏览器将推断出隐式阴影 box-shadow: transparent 0 0 0box-shadow: inset transparent 0 0 0,替换任何未显式包含在前或后过渡状态中的值。这些示例在本书的章节文件中

只有可动画化的属性值更改会触发transitionend事件。

如果您意外包含了一个不能过渡的属性,不要担心。整个声明不会失败:浏览器只是不会过渡不可动画化的属性。

请注意,非可动画属性或不存在的 CSS 属性并非完全被忽略。浏览器会跳过无法识别或不可动画化的属性,保持它们在属性列表中的位置顺序,以确保后续描述的其他逗号分隔的过渡属性不会应用于错误的属性。^(1)

注意

过渡只能发生在当前未受 CSS 动画影响的属性上。如果元素正在被动画化,属性仍可能过渡,只要它们不是当前由动画控制的属性即可。CSS 动画详见第十九章。

打印过渡

当打印网页或 Web 应用程序时,将使用打印媒体的样式表。如果你的样式元素的 media 属性仅匹配screen,则 CSS 将完全不影响打印页面。

通常不包括 media 属性;就像设置了media="all"一样,默认情况下是这样。取决于浏览器,当打印过渡元素时,可能会忽略插值值,或者打印当前状态中的属性值。

你无法在纸张上看到元素的过渡效果,但在某些浏览器中(如 Chrome),如果一个元素从一个状态过渡到另一个状态,在调用print函数时,打印页面上的值将是过渡时的当前状态,如果该属性是可打印的。例如,如果背景颜色发生变化,则不会打印预过渡或后过渡背景颜色,因为背景颜色通常不会被打印。但是,如果文本颜色从一个值变为另一个值,则颜色的当前值将会被打印在彩色打印机或 PDF 中。

在其他浏览器(如 Firefox)中,无论是预过渡值还是后过渡值的打印取决于过渡是如何启动的。例如,如果是通过悬停启动的,则会打印非悬停状态的值,因为在与打印对话框交互时,你不再悬停在元素上。如果是通过添加类进行过渡,则会打印后过渡值,即使过渡尚未完成。打印时,过渡属性被视为被忽略。

鉴于 CSS 具有单独的打印样式表或@media规则用于打印,浏览器会分别计算样式。在打印样式中,样式不会改变,因此没有任何过渡效果。打印时,属性值被视为瞬间改变而不是随时间过渡。

总结

过渡效果是一种非常有用且强大的方式,用来增强用户界面的体验。不用担心过时的浏览器不支持它们,因为即使浏览器不支持 CSS 过渡,样式重新计算时仍会应用变化。它们只会在初始状态和最终状态之间“瞬间”过渡。用户可能会错过一些有趣(或可能让人烦恼)的效果,但不会错过任何内容。

过渡的定义特征是它们在元素从一种状态过渡到另一种状态时应用,无论是因为用户操作还是某种对 DOM 的脚本更改。如果您希望元素无论用户操作还是 DOM 更改都能动画化,下一章将为您展示方法。

^(1) 这可能会改变。CSS 工作组正在考虑使所有属性值可动画化,如果在前后值之间没有中间点,它们将在时间函数的中点之间切换到下一个值。

第十九章:动画

在上一章介绍的 CSS 过渡中,简单的动画是由 DOM 状态的变化触发的,并从开始状态过渡到结束状态。CSS 动画 类似于过渡,因为 CSS 属性值随时间变化,但动画可以更精确地控制这些变化的方式。具体来说,CSS 关键帧动画让我们决定动画是否重复,以及如何重复,还可以详细控制动画的每个阶段等。虽然过渡触发隐含的属性值变化,但动画则在应用关键帧动画时明确执行。

使用 CSS 动画,您可以更改元素的非预设或后置状态的属性值。在动画元素上设置的属性值不一定需要是动画过程中的一部分。例如,使用过渡时,从黑色到白色只会动画过各种灰色阶段。但是在动画中,该元素不必在动画期间始终是黑色、白色或甚至中间色。

虽然您可以通过灰度过渡,但您也可以将元素变为黄色,然后从黄色动画到橙色。或者,您可以通过各种颜色进行动画,从黑色开始,到白色结束,但在此过程中穿过整个彩虹。

提示

查找播放符号 ,以了解在线示例是否可用。本章中的所有示例都可以在 https://meyerweb.github.io/csstdg5figs/19-animation 找到。

适应癫痫和前庭障碍

警告

虽然您可以使用动画创建不断变化的内容,但是重复快速变化的内容可能会导致某些用户癫痫发作。请始终记住这一点,并确保您的网站对癫痫和其他癫痫症状的用户具有较好的可访问性。

我们通常不会以警告开始一章,但在这种情况下确实有必要。视觉变化,特别是快速的视觉变化,可能会触发容易引起癫痫的用户的医疗紧急情况。它们还可能会导致容易患前庭障碍(晕动病)的用户感到严重不适。

为了减少或消除这种风险,请使用 prefers-reduced-motion 媒体查询(参见第二十一章)。这允许您在用户为其浏览器或操作系统设置了“减少运动”或类似偏好时应用样式。可以考虑如下方法:

@media (prefers-reduced-motion) {
  * {animation: none !important; transition: none !important;}
}

这将禁用所有动画和过渡效果,假设没有指定其他 !important 的动画(它们不应该有)。这并不是一个微妙或完美的解决方案,但这是第一步。您可以通过将所有动画和过渡效果放在一个媒体块中,以便为不启用运动减少功能的用户隔离,来反转此方法,如下所示:

@media not (prefers-reduced-motion) {
  /* all animations and transitions */
}

并非所有动画都危险或令人迷失方向,对所有用户至少进行一些动画可能是必要的。过渡和动画在告知用户发生了什么变化和引导他们关注特定内容方面非常有帮助。在这种情况下,使用prefers-reduced-motion来减少对理解 UI 至关重要的动画,并关闭那些不必要的动画。

定义关键帧

要对元素进行动画处理,需要引用关键帧动画的名称;为了这样做,我们需要一个命名的关键帧动画。第一步是使用@keyframes at 规则定义这个可重用的 CSS 关键帧动画,从而为我们的动画命名。

@keyframes at 规则包括动画标识符或名称,以及一个或多个关键帧块。每个关键帧块包含一个或多个带有属性-值对声明块的关键帧选择器。整个@keyframes at 规则指定了单次完整动画的行为。动画可以零次或多次迭代,主要取决于animation-iteration-count属性值,我们将在“声明动画迭代次数”中讨论。

每个关键帧块包含一个或多个关键帧选择器。这些是沿动画持续时间的百分比时间位置;它们声明为百分比或使用关键字fromto。以下是动画的通用结构:

@keyframes animation_identifier {
  keyframe_selector {
    property: value;
    property: value;
  }
  keyframe_selector {
    property: value;
    property: value;
  }
}

下面是几个基本示例:

@keyframes fadeout {
    from {
        opacity: 1;
    }
    to {
        opacity: 0;
    }
}

@keyframes color-pop {
    0% {
        color: black;
        background-color: white;
    }
    33% { /* one-third of the way through the animation */
        color: gray;
        background-color: yellow;
    }
    100% {
        color: white;
        background-color: orange;
    }
}

所示的第一组关键帧将元素的opacity设置为1(完全不透明),并将其动画化为0不透明度(完全透明)。第二组关键帧将元素的前景动画化为黑色,背景为白色,然后将前景从黑色动画到灰色,然后是白色,背景从白色动画到黄色,然后是橙色。

注意,关键帧不指定动画应该持续多长时间——这由专门用于此目的的 CSS 属性处理。而是它们说,“从这种状态过渡到那种状态”或“在总动画的这些百分比点上达到这些各种状态”。这就是为什么关键帧选择器总是百分比,或者fromto。如果尝试使用时间值(如1.5s)作为关键帧选择器,将使其无效。

设置关键帧动画

在关键帧集的大括号中,包含一系列带有 CSS 块的关键帧选择器,声明您要动画化的属性。一旦定义了关键帧,您可以通过使用animation-name属性将动画“附加”到元素来“激活”它。我们将很快讨论该属性,在“调用命名动画”中。

从 at 规则声明开始,后跟动画名称和大括号:

@keyframes nameOfAnimation {
...
}

创建的名称是标识符或字符串。最初,关键帧名称必须是标识符,但规范和浏览器也支持带引号的字符串。

标识符是未引用的,并且有特定的规则。你可以使用任何字符 a-zA-Z0-9,连字符 (-),下划线 (_),以及 ISO 10646 字符集中的任何字符 U+00A0 及以上。ISO 10646 是通用字符集;这意味着你可以使用 Unicode 标准中匹配正则表达式 [-_a-zA-Z0-9\u00A0-\u10FFFF] 的任何字符。标识符不能以数字(0–9)开头,也不应以两个连字符开头(尽管某些浏览器允许这样)。一个连字符是可以的,只要它后面不跟随数字,除非你用反斜杠转义数字或连字符。

如果你的动画名称中包含任何转义字符,请确保用反斜杠 (\) 转义它们。例如,Q&A! 必须写成 Q\&A\!。名称 ✎ 可以保留为 ✎(不,这不是拼写错误),而 也是一个有效的名称。但是,如果你要在标识符中使用任何不是字母或数字的键盘字符,比如 !@#$ 等,记得用反斜杠转义它们。

此外,在动画名称中不要使用本章中涵盖的任何关键字。例如,我们稍后将讨论的各种动画属性的可能值包括 nonepausedrunninginfinitebackwardsforwards。尽管规范没有禁止使用动画属性关键字作为动画名称,但这很可能会破坏你的动画效果,特别是在使用 animation 简写属性时(在 “汇总” 中讨论)。因此,尽管你可以合法地将你的动画命名为 paused(或其他关键字),我们强烈建议不要这样做。

定义关键帧选择器

关键帧选择器 定义了动画过程中我们想要动画化的属性值。如果你想要在动画开始时设置一个数值,你需要在 0% 处声明它。如果你想在动画结束时设置一个不同的数值,你需要在 100% 处声明属性值。如果你想要在动画进行到三分之一时设置一个数值,你需要在 33% 处声明它。这些标记是由关键帧选择器定义的。

关键帧选择器由一个逗号分隔的百分比值或关键字 fromto 的列表组成。关键字 from 等同于 0%。关键字 to 等同于 100%。关键帧选择器用于指定动画时段内关键帧所代表的百分比。关键帧本身由在选择器上声明的属性值块指定。百分比值必须使用 % 单位。换句话说,0 作为关键帧选择器是无效的:

@keyframes W {
    from {  /* equivalent to 0% */
      left: 0;
      top: 0;
    }
    25%, 75% {
      top: 100%;
    }
    50% {
      top: 50%;
    }
    to {  /* equivalent to 100% */
      left: 100%;
      top: 0;
    }
}

当此@keyframes动画命名为W并附加到非静态定位元素时,会使该元素沿着 W 形路径移动。W有五个关键帧:分别位于0%25%50%75%100%的标记。from0%标记,而to100%标记。

因为我们为25%75%标记设置的属性值相同,所以我们可以将这两个关键帧选择器放在一个逗号分隔的列表中。这与常规选择器非常相似,你可以用逗号将它们组合在一起。无论你将这些选择器保留在一行(如示例中)还是将每个选择器放在自己的一行中,都取决于个人偏好。

注意,关键帧选择器不需要按升序列出。在前面的示例中,我们在同一行上有25%75%,然后是50%标记。为了可读性,强烈建议从0%100%的标记递进。然而,就像本例中的75%关键帧所示,这并不是必需的。你可以从最后一个开始定义你的关键帧,然后从第一个开始,或者随意打乱它们,或者任何你认为适合的方式。

省略fromto

如果没有指定0%from关键帧,则用户代理(浏览器)会构建一个0%关键帧。隐式的0%关键帧使用被动画化属性的原始值,就好像在元素上未应用动画时声明了具有相同属性值的0%关键帧一样,即除非另一个动画正在该元素上动画化相同的属性(详情见“调用命名动画”)。同样地,如果未定义100%to关键帧且未应用其他动画,则浏览器将使用元素在未设置动画时的值创建一个伪100%关键帧。

假设我们有一个background-color的变化动画:

@keyframes change_bgcolor {
    45% { background-color: green; }
    55% { background-color: blue; }
}

如果元素原本设置了background-color: red,则动画就好像如下所示:

@keyframes change_bgcolor {
    0%   { background-color: red; }
    45%  { background-color: green; }
    55%  { background-color: blue; }
    100% { background-color: red; }
}

或者,记住我们可以将多个相同的关键帧作为逗号分隔的列表包含,这个伪动画也可以像这里显示的那样编写:

@keyframes change_bgcolor {
    0%, 100% { background-color: red; }
    45%  { background-color: green; }
    55%  { background-color: blue; }
}

注意,background-color: red;的声明不是原始关键帧动画的一部分;它们只是为了清晰起见在此处填写。我们可以在许多元素上包含这个change_bgcolor动画,且感知到的动画会根据非动画状态下元素background-color属性的值而有所不同。因此,具有黄色背景的元素将从黄色动画到绿色再到蓝色,然后回到黄色。

尽管我们一直在使用百分比的整数值作为百分比,但非整数百分比值(例如33.33%)是完全有效的。负百分比、大于100%的值以及不是百分比或关键字tofrom的值都是无效的,并将被忽略。

重复关键帧属性

就像 CSS 的其余部分一样,具有相同关键帧值的关键帧声明块中的值是级联的。因此,可以通过两次声明to100%来编写先前的W动画,从而覆盖left属性的值:

@keyframes W {
  from, to {
    top: 0;
    left: 0;
  }
  25%, 75% {
    top: 100%;
  }
  50% {
    top: 50%;
  }
  to {
    left: 100%;
  }
}

请注意,在第一个代码块的关键帧选择器中,tofrom一起声明?这为to关键帧设置了topleft。然后,最后一个关键帧块中覆盖了toleft值。

可动画化属性

值得注意的是,并非所有属性都可以动画化。如果在动画的关键帧中列出了无法动画化的属性,它们会被简单地忽略。(同样,浏览器完全不认识的属性和值也会被忽略,就像 CSS 的其他部分一样。)

注意

中点规则的例外包括animating-timing-functionvisibility,这些在下一节中讨论。

只要可动画化属性在至少一个具有与非动画化属性值不同的值的块中包含,并且这两个值之间有可计算的中点,该属性就会动画化。

如果动画设置在两个属性值之间,这两个值之间没有可计算的中点,那么属性可能无法正确或根本无法动画化。例如,不应该声明元素的高度在height: autoheight: 300px之间进行动画,因为auto300px之间没有明确定义的中点。元素仍会动画化,但浏览器会在动画的一半时从预动画状态跳到后动画状态。因此,对于 1 秒的动画,元素将在动画的 500 毫秒处从auto高度跳到300px高度。 其他属性可能会在相同的动画长度内动画化;例如,如果更改背景颜色,它会在动画过程中平滑过渡。只有不能在其间进行动画的属性会在一半时跳跃。

如果您为每个要动画化的属性声明了 0%和 100%的值,则动画的行为将更可预测。例如,如果在动画中声明了border-radius: 50%;,您可能也想声明border-radius: 0%;,因为border-radius的默认值是none而不是0none与其他值之间没有中点。考虑以下两个动画之间的差异:

@keyframes round {
    100% {
        border-radius: 50%;
    }
}
@keyframes square_to_round {
    0% {
        border-radius: 0%;
    }
    100% {
        border-radius: 50%;
    }
}

round 动画将从元素的原始 border-radius 值动画到 border-radius: 50% 的值。square_to_round 动画将从 border-radius: 0% 动画到 border-radius: 50%。如果元素起始为方形角,两个动画将完全相同。但是如果元素起始为圆角,square_to_round 将在开始动画之前跳转到矩形角。

使用非可动画属性,这些属性不会被忽略

例外情况中点规则包括 visibilityanimation-timing-function

visibility 属性是可动画化的,即使 visibility: hiddenvisibility: visible 之间没有中点。当您从 hidden 动画到 visible 时,可见性值在声明变化的关键帧上瞬间跳转。因此,您不会得到从可见到隐藏或反之的平滑淡入淡出效果。状态会瞬间变化。

虽然 animation-timing-function 实际上并非可动画化属性,但当包含在关键帧块中时,动画时间将在动画的该点切换到新声明的值,仅适用于该关键帧选择器块内的属性。动画时间的变化不是动画的;它只是简单地切换到这些属性的新值,并且仅持续到下一个关键帧。这使得您可以在一个关键帧与另一个关键帧之间变化时间函数。(详见“更改动画的内部时间”。)

脚本化 @keyframes 动画

CSSKeyframesRule API 允许查找、追加和删除关键帧规则。您可以使用 appendRule(n)deleteRule(n) 改变给定 @keyframes 声明中关键帧块的内容,其中 n 是该关键帧的完整选择器。您可以使用 findRule(n) 返回关键帧的内容。考虑以下情况:

@keyframes W {
  from, to { top: 0; left: 0; }
  25%, 75% { top: 100%; }
  50%      { top: 50%; }
  to       { left: 100%; }
}

appendRule()deleteRule()findRule() 方法以完整的关键帧选择器作为参数,如下所示:

// Get the selector and content block for a keyframe
var aRule = myAnimation.findRule('25%, 75%').cssText;

// Delete the 50% keyframe
myAnimation.deleteRule('50%');

// Add a 53% keyframe to the end of the animation
myAnimation.appendRule('53% {top: 50%;}');

语句 myAnimation.findRule('25%, 75%').cssText 中,myAnimation 指向关键帧动画,则返回与 25%, 75% 匹配的关键帧。它不会匹配任何仅使用 25%75% 的块。如果 myAnimation 指向 W 动画,则 myAnimation.findRule('25%, 75%').cssText 返回 25%, 75% { top: 100%; }

类似地,myAnimation.deleteRule('50%') 将删除最后一个 50% 关键帧 — 如果我们有多个 50% 关键帧,则列出的最后一个将首先被删除。相反,myAnimation.appendRule('53% {top: 50%;}')@keyframes 块的最后一个关键帧之后追加一个 53% 关键帧。

CSS 有四个动画事件:animationstartanimationendanimationiterationanimationcancel。前两者发生在动画开始和结束时,最后一个在迭代结束和下一次迭代开始之间触发。任何定义了有效关键帧规则的动画都会生成开始和结束事件,即使是空的关键帧规则的动画也是如此。animationiteration事件仅在动画有多个迭代时发生,因为如果animationend事件与之同时发生,则animationiteration事件不会触发。animationcancel事件在运行中的动画在达到最后关键帧之前停止时触发。

对元素进行动画处理

一旦您创建了关键帧动画,就可以将该动画应用于元素和/或伪元素。CSS 提供了许多动画属性来将关键帧动画附加到元素并控制其进度。至少,您需要为元素动画包含动画的名称,并在希望动画可见时指定持续时间。(如果没有持续时间,动画将在零时间内完成。)

您可以通过两种方式将动画属性附加到元素:分别包含所有动画属性,或者使用animation简写属性(或简写和长手写结合)。让我们从单独的属性开始。

调用命名动画

animation-name属性的值是要应用于所选元素的关键帧动画名称的逗号分隔列表。这些名称是您在@keyframes规则中创建的未引用的标识符或引用的字符串(或两者的混合)。

默认值是none,这意味着未对所选元素应用任何动画。none值可用于覆盖 CSS 层叠中的任何其他动画。(这也是为什么你不想将你的动画命名为none,除非你是一个受虐狂。)

使用在“省略 from 和 to 值”中定义的change_bgcolor关键帧动画,我们有以下内容:

 div {
     animation-name: change_bgcolor;
 }

这个简单规则将change_bgcolor动画应用到所有<div>元素上,无论页面上有多少个或多少个。要应用多个动画,请包含多个逗号分隔的动画名称:

 div {
    animation-name: change_bgcolor, round, W;
 }

如果包含的关键帧标识符之一不存在,则动画系列不会失败;相反,失败的动画将被忽略,并且有效的动画将被应用。虽然最初被忽略,但如果该关键帧动画随后作为有效动画存在时,将被应用。考虑以下情况:

 div {
    animation-name: change_bgcolor, spin, round, W;
 }

在这个例子中,假设没有定义spin关键帧动画。spin动画将不会被应用,而change_bgcolorroundW动画将会发生。如果通过脚本存在spin关键帧动画,则会在那时应用。

如果一个元素应用了多个动画,并且这些动画具有重复的属性,则后面的动画会覆盖先前动画中的属性值。例如,如果两个不同的关键帧动画同时应用了超过两个背景颜色更改,那么排在后面的动画将覆盖动画列表中先前动画的背景属性声明,但仅当这些属性(在本例中为背景颜色)同时被动画化时。欲了解更多,请参阅“动画、特异性和优先级顺序”。

例如,假设如下,并进一步假设动画发生在 10 秒内:

div {animation-name: change_bgcolor, bg-shift;}

@keyframes bg-shift {
    0%, 100% {background-color: cyan;}
    35% {background-color: orange;}
    55% {background-color: red;}
    65% {background-color: purple;}
}
@keyframes change_bgcolor {
    0%, 100% {background-color: yellow;}
    45% {background-color: green;}
    55% {background-color: blue;}
}

背景颜色将从青色渐变到橙色,再到红色,最后到紫色,然后再回到青色,这要归功于bg-shift。因为它在动画列表中排在最后,所以其关键帧优先生效。每当多个动画在同一时间点为同一属性指定行为时,animation-name值列表中排在最后的动画将生效。

有趣的是,如果强制动画中省略了from0%)或to100%)关键帧会发生什么。例如,让我们移除bg-shift中定义的第一个关键帧:

div {animation-name: change_bgcolor, bg-shift;}

@keyframes bg-shift {
    35% {background-color: orange;}
    55% {background-color: red;}
    65% {background-color: purple;}
}
@keyframes change_bgcolor {
    0%, 100% {background-color: yellow;}
    45% {background-color: green;}
    55% {background-color: blue;}
}

现在,在bg-shift的开始和结束处没有定义背景颜色。在这种情况下,当未指定0%100%关键帧时,用户代理会通过使用正在动画化的属性的计算值来构建0%/100%关键帧。

这些只是在两个不同的关键帧块试图改变同一属性值时的考虑。在这种情况下,该属性是background-color。另一方面,如果一个关键帧块动画化了background-color,而另一个动画化了padding,则这两个动画不会发生冲突,背景颜色和填充都会一起动画化。

仅仅将动画应用到一个元素并不足以使元素可见动画化。为了实现动画效果,动画必须在一段时间内发生。为此,我们有animation-duration属性。

定义动画长度

animation-duration属性定义了单个动画迭代应该花费的时间,单位可以是秒(s)或毫秒(ms)。

animation-duration属性定义了动画完成所有关键帧循环所需的时间长度,单位可以是秒(s)或毫秒(ms)。如果不声明animation-duration,动画将仍然以0s的持续时间运行,尽管动画是不可察觉的,animationstartanimationend仍将被触发。animation-duration不允许负时间值。

在指定持续时间时,必须包括秒(s)或毫秒(ms)单位。如果有多个动画,可以为每个动画包括不同的animation-duration,通过包括逗号分隔的多个时间持续值:

div {
   animation-name: change_bgcolor, round, W;
   animation-duration: 200ms, 100ms, 0.5s;
}

如果在持续时间的逗号分隔列表中提供了无效值(例如animation-duration: 200ms, 0, 0.5s),整个声明将失败,并且会表现为声明了animation-duration: 0s一样;0不是有效的时间值。

通常,对于提供的每个animation-name,您会想要包含一个animation-duration值。如果只有一个持续时间,所有动画将持续相同的时间。在逗号分隔的属性值列表中,如果animation-duration值少于animation-name值,不会失败:而是作为一个组重复这些值。假设我们有以下情况:

div {
    animation-name: change_bgcolor, spin, round, W;
    animation-duration: 200ms, 5s;
       /* same effect as '200ms, 5s, 200ms, 5s' */
}

change_bgcolorround动画将持续200msspinW动画将持续5s

如果animation-duration值多于animation-name值,额外的值将被忽略。如果其中一个动画不存在,动画序列和动画持续时间不会失败;失败的动画及其持续时间将被忽略:

div {
    animation-name: change_bgcolor, spinner, round, W;
    animation-duration: 200ms, 5s, 100ms, 0.5s;
}

在这个例子中,持续时间5sspinner相关联。不过,spinner并不存在,因此5sspinner都将被忽略。如果spinner动画出现,它将应用于<div>元素,并持续 5 秒。

声明动画迭代

只需包含所需的animation-name将导致动画播放一次,仅一次,在动画结束时重置到初始状态。如果希望比默认的一次更多或更少地迭代动画,请使用animation-iteration-count属性。

默认情况下,动画将只播放一次(因为默认值为1)。如果为animation-iteration-count指定了其他值,并且animation-delay属性没有负值,则动画将重复指定属性值的次数,该值可以是任意数字或关键字infinite。以下声明将导致它们的动画分别重复 2 次、5 次和 13 次:

animation-iteration-count: 2;
animation-iteration-count: 5;
animation-iteration-count: 13;

如果 animation-iteration-count 的值不是整数,则动画仍会运行,但会在最后一次迭代中间截断。例如,animation-iteration-count: 1.25 会使动画运行一次并且四分之一次,即第二次迭代进行到 25% 的时候结束。如果值为 0.25,则在 8 秒动画中大约播放 25% 的时间后结束,总共 2 秒。

不允许使用负数。如果提供了无效的值,则默认值 1 会导致默认的单次迭代。

有趣的是,0animation-iteration-count 属性的有效值。当设置为 0 时,动画仍然会发生,但不执行任何次数。这类似于设置 animation-duration: 0s:它会触发 animationstartanimationend 事件。

如果你要将多个动画附加到一个元素或伪元素上,请在 animation-nameanimation-durationanimation-iteration-count 的值中包含逗号分隔的列表:

.flag {
    animation-name: red, white, blue;
    animation-duration: 2s, 4s, 6s;
    animation-iteration-count: 3, 5;
}

iteration-count 值(和所有其他动画属性值)将按逗号分隔的 animation-name 属性值的顺序赋值。多余的值将被忽略。缺少的值会导致现有值重复,就像前述情况中的 animation-iteration-count 一样。

前面的例子中名称值比计数值多,因此计数值将重复:redblue 将迭代三次,而 white 将迭代五次。名称值和持续时间值数量相同,因此持续时间值不会重复。red 动画持续 2 秒,迭代三次,因此总计运行 6 秒。white 动画持续 4 秒,迭代五次,总计运行 20 秒。blue 动画每次迭代 6 秒,重复三次,总共动画了 18 秒。

无效的值会使整个声明无效,导致每个动画只播放一次。

如果我们希望所有三个动画在相同的时间结束,即使它们的持续时间不同,我们可以通过 animation-iteration-count 控制:

.flag {
    animation-name: red, white, blue;
    animation-duration: 2s, 4s, 6s;
    animation-iteration-count: 6, 3, 2;
}

在这个例子中,redwhiteblue 动画每个持续 12 秒,因为每种情况下持续时间和迭代次数的乘积总共为 12 秒。

你还可以用关键词 infinite 替代数字,表示动画将永远迭代,或者直到某些条件使其停止,比如移除动画名称、从 DOM 中移除元素或者暂停播放状态。

设置动画方向

使用animation-direction属性,您可以控制动画是从 0%关键帧到 100%关键帧,还是从 100%关键帧到 0%关键帧进行。您还可以定义所有迭代是否按相同方向进行,或者设置每隔一个动画周期以相反方向进行。

animation-direction属性定义了动画通过关键帧的进度方向。它有四个可能的值:

normal

动画的每个迭代都从 0%关键帧进展到 100%关键帧;这个值是默认值。

reverse

将每个迭代设置为以反向关键帧顺序播放,始终从 100%关键帧进展到 0%关键帧。反转动画方向还会反转animation-timing-function(在“更改动画的内部时间”中描述)。

alternate

第一个迭代(以及每个后续的奇数迭代)从 0%到 100%进行,而第二个迭代(以及每个后续的偶数周期)将反向进行,从 100%到 0%进行。只有当你有多个迭代时才会产生影响。

alternate-reverse

alternate值类似,只是它是反向的。第一个迭代(以及每个后续的奇数迭代)将从 100%到 0%进行,而第二个迭代(以及每个后续的偶数周期)将反向进行,从 100%到 0%进行:

.ball {
    animation-name: bouncing;
    animation-duration: 400ms;
    animation-iteration-count: infinite;
    animation-direction: alternate-reverse;
}
@keyframes bouncing {
    from {
        transform: translateY(500px);
    }
    to {
        transform: translateY(0);
    }
}

在这个例子中,我们弹跳一个球,但我们想从扔它而不是向上抛它开始:我们希望它在下落和上升之间交替,而不是上升和下降,所以animation-direction: alternate-reverse是我们需求的最合适的值。

这是一个制作球反弹的基本方法。当球反弹时,它们在达到最高点时移动最慢,在达到最低点时移动最快。我们在这里包含这个例子是为了说明alternate-reverse动画方向。我们将重新审视弹跳动画,通过添加时间(在“更改动画的内部时间”中)使其更加逼真。我们还将讨论当动画以反向迭代时,animation-timing-function如何反转。

延迟动画

animation-delay属性定义了动画附加到元素后在开始第一次动画迭代之前浏览器等待的时间。

默认情况下,动画在附加到元素时立即开始迭代,延迟为 0 秒。animation-delay为正值将延迟动画的开始,直到属性值所列时间已过。

animation-delay的负值是允许的,并且能产生有趣的效果。负延迟会立即执行动画,但会在所附的动画的中途开始动画元素。例如,如果在一个元素上设置了animation-delay: -4sanimation-duration: 10s,动画会立即开始,但会大约从第一个动画的 40%处开始,并在之后的 6 秒结束。

我们说大约,因为动画不一定会准确地从 40%关键帧块开始:动画的 40%标记发生的时间取决于animation-timing-function的值。如果设置了animation-timing-function: linear,动画状态将从动画的 40%处开始:

div {
  animation-name: move;
  animation-duration: 10s;
  animation-delay: -4s;
  animation-timing-function: linear;
}

@keyframes move {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(1000px);
  }
}

在这个linear动画示例中,我们有一个持续 10 秒的动画,延迟为-4 秒。在这种情况下,动画将立即开始,从动画的 40%处开始,<div>元素向右移动 400 像素,持续时间仅为 6 秒。

如果动画设置为发生 10 次,延迟为-600 毫秒,动画持续时间为 200 毫秒,元素将立即开始动画,即在第四次迭代开始时:

.ball {
  animation-name: bounce;
  animation-duration: 200ms;
  animation-delay: -600ms;
  animation-iteration-count: 10;
  animation-timing-function: ease-in;
  animation-direction: alternate;
}
@keyframes bounce {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(500px);
  }
}

而不是以正常方向开始进行 2000 毫秒(200 毫秒 × 10 = 2000 毫秒,或 2 秒)的动画,球将以 1400 毫秒(或 1.4 秒)的动画时间立即开始,但在第四次迭代的开始处,并且是在反向方向上。

动画从反向开始,因为animation-direction设置为alternate,意味着每个偶数次迭代从 100%关键帧到 0%关键帧。第四次迭代,即偶数次迭代,是第一个可见的迭代。

在这种情况下,动画将立即抛出animationstart事件。animationend事件将在 1400 毫秒时发生。球会被抛起,而不是弹跳,产生六次animationiteration事件,分别在 200、400、600、800、1000 和 1200 毫秒后。虽然迭代次数设置为 10,但只有六次animationiteration事件,因为由于负的animation-delay,有三次迭代不会发生,并且最后一次迭代与animationend事件同时结束。请记住,当animationiteration事件与animationend事件同时发生时,animationiteration事件不会发生。

在继续之前,让我们深入了解动画事件。

探索动画事件

三种类型的动画事件是animationstartanimationiterationanimationend。每个事件都有三个只读属性:animationNameelapsedTimepseudoElement

animationstart事件在动画开始时触发:在animation-delay(如果存在)过期后触发,如果没有设置延迟,则立即触发。如果存在负的animation-delay值,则animationstart将立即触发,elapsedTime等于延迟的绝对值。

animationend事件在动画结束时触发。如果animation-iteration-count设置为infinite,则只要animation-duration设置为大于0的时间,animationend事件就永远不会触发。如果animation-duration设置或默认为 0 秒,即使迭代次数为无限,animationstartanimationend也会几乎同时发生,且顺序如上。

.noAnimationEnd {
    animation-name: myAnimation;
    animation-duration: 1s;
    animation-iteration-count: infinite;
}
.startAndEndSimultaneously {
    animation-name: myAnimation;
    animation-duration: 0s;
    animation-iteration-count: infinite;
}

animationiteration事件在迭代之间触发。animationend事件在不同时发生的迭代的结束时触发;因此,animationiterationanimationend事件不会同时触发。

.noAnimationIteration {
    animation-name: myAnimation;
    animation-duration: 1s;
    animation-iteration-count: 1;
}

.noAnimationIteration示例中,将animation-iteration-count设置为一次时,动画在第一次迭代结束时结束。每当animationiteration事件与animationend事件同时发生时,animationend事件会发生,但animationiteration事件不会发生。

当省略animation-iteration-count属性或其值为1或更少时,不会触发animationiteration事件。只要一个迭代完成(即使是部分迭代)并且另一个迭代开始,如果后续迭代的持续时间大于0s,将触发animationiteration事件。

.noAnimationIteration {
    animation-name: myAnimation;
    animation-duration: 1s;
    animation-iteration-count: 4;
    animation-delay: -3s;
}

当动画由于负的animation-delay而迭代的周期少于animation-iteration-count中列出的周期时,未发生的周期没有animationiteration事件。前面的示例代码没有animationiteration事件,因为前三个周期不会发生(由于-3sanimation-delay),最后一个周期在动画结束时同时完成。

在该示例中,animationstart事件的elapsedTime3,因为它等于延迟的绝对值。

动画串联

你可以使用animation-delay来串联动画,使下一个动画在前一个动画结束后立即开始:

.rainbow {
    animation-name: red, orange, yellow, blue, green;
    animation-duration: 1s, 3s, 5s, 7s, 11s;
    animation-delay: 3s, 4s, 7s, 12s, 19s;
}

在这个示例中,red动画在延迟 3 秒后开始,并持续 1 秒,这意味着animationend事件在第 4 秒发生。这个示例在每个后续动画开始时都会接续前一个动画。这被称为CSS 动画链接

在第二个动画上包含 4 秒的延迟,orange动画将在第 4 秒标记处开始插值@keyframe属性值,立即在red动画结束时开始orange动画。orange动画在第 7 秒结束,它持续 3 秒,从第三个或yellow动画上设置的延迟开始,使得yellow动画在orange动画结束后立即开始。

这是在单个元素上链接动画的一个示例。你也可以使用animation-delay属性来链接不同元素的动画:

li:first-of-type {
    animation-name: red;
    animation-duration: 1s;
    animation-delay: 3s;
}
li:nth-of-type(2) {
    animation-name: orange;
    animation-duration: 3s;
    animation-delay: 4s;
}
li:nth-of-type(3)  {
    animation-name: yellow;
    animation-duration: 5s;
    animation-delay: 7s;
}
li:nth-of-type(4) {
    animation-name: green;
    animation-duration: 7s;
    animation-delay: 12s;
}
li:nth-of-type(5) {
    animation-name: blue;
    animation-duration: 11s;
    animation-delay: 19s;
}

如果你想让一组列表项按顺序动画, 看起来像是动画按顺序链接,每个列表项的animation-delay应该是前一个动画的animation-durationanimation-delay的总和。

虽然你可以使用 JavaScript 和从一个动画的animationend事件来确定何时附加下一个动画(我们稍后会讨论),但是animation-delay属性是使用 CSS 动画属性链接动画的合适方法。有一个警告:动画是 UI 线程上的最低优先级。因此,如果你有一个占用 UI 线程的脚本在运行,根据浏览器和哪些属性正在被动画化以及元素上设置了什么属性值,浏览器可能会在等待 UI 线程可用之前让延迟过期,然后再开始更多动画。

如果你能依赖 JavaScript,另一种链接动画的方法是监听animationend事件来启动后续动画:

<script>
  document.querySelectorAll('li')[0].addEventListener( 'animationend',
    () => {
        document.querySelectorAll('li')[1].style.animationName = 'orange';
    },
    false );

  document.querySelectorAll('li')[1].addEventListener( 'animationend',
    () => {
        document.querySelectorAll('li')[2].style.animationName = 'yellow';
    },
    false );

  document.querySelectorAll('li')[2].addEventListener( 'animationend',
    () => {
        document.querySelectorAll('li')[3].style.animationName = 'green';
    },
    false );

  document.querySelectorAll('li')[3].addEventListener( 'animationend',
    () => {
        document.querySelectorAll('li')[4].style.animationName = 'blue';
    },
    false );
</script>

<style>
  li:first-of-type {
    animation-name: red;
    animation-duration: 1s;
  }
  li:nth-of-type(2) {
    animation-duration: 3s;
  }
  li:nth-of-type(3)  {
    animation-duration: 5s;
  }
  li:nth-of-type(4) {
    animation-duration: 7s;
  }
  li:nth-of-type(5)  {
    animation-duration: 11s;
  }
</style>

在这个示例中,前四个列表项都有一个事件处理程序,监听该列表项的animationend事件。当animationend事件发生时,事件监听器会向后续的列表项添加一个animation-name

正如你在样式中看到的那样,这种动画链接方法根本不使用animation-delay。相反,JavaScript 事件监听器在抛出animationend事件时通过设置animation-name属性向每个元素附加动画。

您还会注意到,animation-name仅包含在第一个列表项中。其他列表项仅具有animation-duration而没有animation-name,因此没有附加的动画。通过 JavaScript 添加animation-name是附加并启动动画的方法,至少在此示例中是这样。要启动或重新启动动画,必须删除然后重新添加动画名称,此时所有动画属性均生效,包括animation-delay

而不是写以下内容:

<script>
  document.querySelectorAll('li')[2].addEventListener( 'animationend',
    () => {
        document.querySelectorAll('li')[3].style.animationName = 'green';
    },
    false );

  document.querySelectorAll('li')[3].addEventListener( 'animationend',
    () => {
        document.querySelectorAll('li')[4].style.animationName = 'blue';
    },
    false );
</script

<style>
  li:nth-of-type(4) {
    animation-duration: 7s;
  }
  li:nth-of-type(5)  {
    animation-duration: 11s;
  }
</style>

我们可以写成这样:

<script>
  document.querySelectorAll('li')[2].addEventListener( 'animationend',
    () => {
        document.querySelectorAll('li')[3].style.animationName = 'green';
        document.querySelectorAll('li')[4].style.animationName = 'blue';
    },
  false );
</script>

<style>
  li:nth-of-type(4) {
    animation-duration: 7s;
  }
  li:nth-of-type(5)  {
    animation-delay: 7s;
    animation-duration: 11s;
  }
</style>

blue动画名称与我们添加green同时添加到第五个列表项时,第五个元素上的延迟会在此时生效并开始计时。

虽然在动画过程中改变元素上的动画属性值(除名称外)不会影响动画,但添加或移除animation-name确实会产生影响。您无法在动画中间将动画持续时间从100ms更改为400ms。一旦已经应用了延迟,您就无法将延迟从-200ms切换到5s。但是,您可以通过删除并重新应用来停止和启动动画。在前面的 JavaScript 示例中,我们通过将动画应用于元素来启动动画。

另外,将display: none设置为一个元素会终止任何动画。将display更新回可见值会从头开始动画。如果animation-delay有一个正值,那么在animationstart事件发生和任何动画发生之前必须等待延迟时间。如果延迟是负值,则动画将在迭代的中途开始,正如任何其他应用动画一样。

动画迭代延迟

什么是动画迭代延迟?有时您希望动画多次发生,但想在每次迭代之间等待特定的时间。

虽然不存在动画迭代延迟属性,但可以使用animation-delay属性,在关键帧声明中加入延迟,或使用 JavaScript 来模拟。模拟的最佳方法取决于迭代次数、性能以及延迟是否均匀。

假设您希望您的元素增长三次,但希望在每个 1 秒的迭代之间等待 4 秒钟。您可以在关键帧定义中包含延迟,并通过它进行三次迭代:

.animate3times {
    background-color: red;
    animation: color_and_scale_after_delay;
    animation-iteration-count: 3;
    animation-duration: 5s;
}

@keyframes color_and_scale_after_delay {
    80% {
        transform: scale(1);
        background-color: red;
    }
    80.1% {
        background-color: green;
        transform: scale(0.5);
    }
    100% {
        background-color: yellow;
        transform: scale(1.5);
    }
}

注意,第一个关键帧选择器位于 80%的位置,并与默认状态匹配。 这将使您的元素动画化三次:它在 5 秒动画的 80%时间内保持默认状态(4 秒内不变化),然后在动画的最后 1 秒内从绿色变为黄色、从小变大,然后再次迭代,在三次迭代后停止。

这种方法适用于动画的任意迭代次数。不幸的是,它只在每次迭代之间的延迟相同且不想以任何其他时序重复使用动画时才是一个好的解决方案,例如 6 秒的延迟。 如果你想要改变每次迭代之间的延迟而不改变大小和颜色变化的持续时间,你必须编写一个新的@keyframes定义。

要在动画之间启用多个迭代延迟,我们可以创建一个单独的动画,并将三种不同延迟的效果编码到动画关键帧定义中:

.animate3times {
    background-color: red;
    animation: color_and_scale_3_times;
    animation-iteration-count: 1;
    animation-duration: 15s;
}

@keyframes color_and_scale_3_times {
  0%, 13.32%, 20.01%, 40%, 46.67%, 93.32% {
        transform: scale(1);
        background-color: red;
  }
    13.33%, 40.01%, 93.33% {
        background-color: green;
        transform: scale(0.5);
  }
    20%, 46.66%, 100% {
        background-color: yellow;
        transform: scale(1.5);
  }
}

然而,这种方法可能更难编码和维护。 它仅适用于动画的单个周期。要更改动画的数量或迭代延迟持续时间,需要另一个@keyframes声明。这个示例甚至比前一个更不健壮,但它确实允许在迭代之间使用不同的延迟。

一个特别允许在动画规范中的解决方案是:多次声明动画,每次使用不同的animation-delay值:

.animate3times {
  animation: color_and_scale, color_and_scale, color_and_scale;
  animation-delay: 0, 4s, 10s;
  animation-duration: 1s;
}

@keyframes color_and_scale {
    0% {
        background-color: green;
        transform: scale(0.5);
    }
    100% {
        background-color: yellow;
        transform: scale(1.5);
    }
}

在这里,我们已经将动画附加了三次,每次使用不同的延迟。在这种情况下,每次动画迭代结束后才会进行下一次。

如果动画在同时进行时重叠,值将是最后声明的动画的值。与同时更改元素属性的多个动画一样,序列中最后声明的动画将覆盖列表中之前动画的任何动画。声明三个不同间隔的color_and_scale动画,color_and_scale动画的最后一次迭代的属性值将覆盖尚未结束的前几次迭代的值。

模拟动画迭代延迟属性的最安全、最健壮且最跨浏览器友好的方法是使用 JavaScript 的动画事件。在animationend时从元素上分离动画,然后在迭代延迟后重新附加它。如果所有迭代延迟都相同,可以使用setInterval;如果它们不同,使用setTimeout

let iteration = 0;
const el = document.getElementById('myElement');

el.addEventListener('animationend', () => {
  let time = ++iteration * 1000;

  el.classList.remove('animationClass');

  setTimeout( () => {
    el.classList.add('animationClass');
  }, time);

});

改变动画的内部时序

好的!脚本编写很有趣,但让我们回到纯 CSS 并谈论时间函数。类似于transition-timing-function属性,animation-timing-function属性描述了动画如何从一个关键帧进展到下一个。

除了步进时间函数,描述在“使用步进时间函数”中,所有时间函数都是贝塞尔曲线。就像transition-timing-function一样,CSS 规范提供了五个预定义的贝塞尔曲线关键字,我们在前一章中描述了这些(参见表 18-1 和图 18-3)。

一个方便的工具用于可视化贝塞尔曲线并创建自定义曲线是Lea Verou 的贝塞尔曲线可视化器

默认的ease具有缓慢开始,然后加速,并以缓慢结束。这个函数类似于ease-in-out,其开始时加速度更大。linear时间函数,如其名称所示,创建动画以恒定速度运行。

ease-in时间函数创建一个缓慢开始、加速,然后突然停止的动画。相反的ease-out时间函数从全速开始,然后随着动画迭代的结束逐渐减速。

如果这些都不符合您的需求,您可以通过传递四个值来创建自己的贝塞尔曲线时间函数,如下所示:

animation-timing-function: cubic-bezier(0.2, 0.4, 0.6, 0.8);

虽然x值必须在 0 和 1 之间,但通过使用y值大于 1 或小于 0 的值,您可以创建一个反弹效果,使动画在值之间上下弹跳,而不是在单一方向上连续进行。考虑以下时间函数,其相当奇异的贝塞尔曲线部分显示在图 19-1 中:

.snake {
  animation-name: shrink;
  animation-duration: 10s;
  animation-timing-function: cubic-bezier(0, 4, 1, -4);
  animation-fill-mode: both;
}

@keyframes shrink {
  0% {
    width: 500px;
  }
  100% {
    width: 100px;
  }
}

css5 1901

图 19-1. 一个奇异的贝塞尔曲线

animation-timing-function曲线使动画属性的值超出了在0%100%关键帧中设置的值的范围。在此示例中,我们正在将一个元素从500px缩小到100px。但由于cubic-bezier值的影响,我们要缩小的元素实际上会比在0%关键帧中定义的500px宽度更宽,并且比在100%关键帧中定义的100px宽度更窄,如图 19-2 所示。

css5 1902

图 19-2. 奇异贝塞尔曲线的效果

在这种情况下,元素从500px的宽度开始,在0%关键帧中定义。然后它迅速收缩到约40px的宽度,比100%关键帧中定义的width: 100px还要窄。从那里开始,它慢慢扩展到约750px宽,这比原始的500px宽度还要大。然后它迅速收缩回到width: 100px,结束动画迭代。

你可能已经意识到,我们动画生成的曲线与贝塞尔曲线相同。正如 S 曲线超出了正常边界框一样,动画元素的宽度比我们设置的 100px 更窄,并且比设置的 500px 更宽。

贝塞尔曲线的外观像一条蛇,因为一个 y 坐标为正,另一个为负。如果两个值都是大于 1 的正值或者都是小于–1 的负值,贝塞尔曲线是弧形的,超过或低于设置的一个值,但不像 S 曲线那样两端都超出边界。

任何使用 animation-timing-function 声明的时间函数都设置了正常动画方向的时间,当动画从 0% 关键帧进展到 100% 关键帧时。当动画以反向方式运行时,从 100% 关键帧到 0% 关键帧,动画时间函数被反转。

还记得“动画方向”中的反弹球示例吗?反弹效果并不是很真实,因为原始示例默认使用 ease 作为其时间函数。使用 animation-timing-function,我们可以将 ease-in 应用于动画,使得球在下降时在接近 100% 关键帧时变得更快。当它向上反弹时,动画以相反方向进行,从 100%0%,因此动画时间函数也被反转——在这种情况下是 ease-out,在达到顶点时减慢速度:

.ball {
  animation-name: bounce;
  animation-duration: 1s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in;
  animation-direction: alternate;
}

@keyframes bounce {
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(500px);
  }
}

使用步进时间函数

步进时间函数 step-startstep-endsteps() 不是贝塞尔曲线。它们根本不是曲线。相反,它们是 tweening 定义。在角色或精灵动画中,steps() 函数最有用。

steps() 函数将动画分为一系列相等长度的步骤。该函数接受两个参数:步数和变化点(稍后详细介绍)。

步数参数值必须是正整数。动画长度将被均等地分成提供的步数。例如,如果动画持续时间为 1 秒,步数为 5,则动画将被分为五个 200 毫秒的步骤,元素将在每个间隔的 200 毫秒内重新绘制到页面上,每个间隔移动动画的 20%。

要理解其工作原理,请想象一本翻页书。翻页书中的每一页都包含一幅稍有不同的绘画或图片,就像电影胶片中的一帧图像被印在每一页上一样。当快速翻动翻页书的页面时(因此得名),图片就会呈现出动画效果。通过使用图像雪碧图、background-position 属性和 steps() 时间函数,您可以使用 CSS 创建类似的动画效果。

图 19-3 展示了一个包含几个图像的雪碧图,这些图像仅略有不同,就像翻页书的各个页面上的绘画一样。

css5 1903

图 19-3. 舞蹈雪碧图

我们将所有略有不同的图像放入一个称为 雪碧图 的单个图像中。我们雪碧图中的每个图像都是我们正在创建的单个动画图像中的一帧。

然后我们创建一个容器元素,大小与我们雪碧图中单个图像的大小相同,并将雪碧图作为容器元素的背景图像。我们使用 steps() 时间函数来动画 background-position,这样我们一次只看到雪碧图中变化的单个图像实例。在 steps() 时间函数中的步数是我们雪碧图中图像的数量。步数定义了我们的背景图像在完成单个动画时所需的停止次数。

图 19-3 中的雪碧图包含 22 张图像,每张图像大小为 56 × 100 像素。我们雪碧图的总大小为 1,232 × 100 像素。我们将容器设置为单个图像大小:56 × 100 像素。我们将雪碧图设置为我们的背景图像:background-position 的初始或默认值是 top left,与 0 0 相同。我们的图像将显示在 0 0 处,这是一个良好的默认值。不支持 CSS 动画的浏览器(如 Opera Mini)将仅显示我们雪碧图中的第一张图像:

.dancer {
  height: 100px;
  width: 56px;
  background-image: url(../images/dancer.png);
  ....
}

关键是使用 steps() 来改变 background-position 的值,以便每帧都是雪碧图中单独图像的视图。使用 steps() 时间函数不是从左边滑入背景图像,而是在我们声明的步数中弹出背景图像。

所以我们创建了一个动画,简单地改变了background-position的左右值。图片宽度为 1,232 像素,所以我们将背景图片从0 0,即左上角,移动到0 -1232px,完全超出我们的 56 × 100 像素的 <div> 视口。

-1232px 0 的值将完全将图像移动到左侧,超出我们包含块的视口。除非 background-repeat 设置为沿 x 轴重复,否则它将不会在我们的 100 × 56 像素的 <div> 中的 100% 位置显示为背景图像。我们不希望发生这种情况!

这就是我们想要的效果:

@keyframes dance_in_place {
  from {
      background-position: 0 0;
  }
  to {
      background-position: -1232px 0;
  }
}

.dancer {
  ....
  background-image: url(../images/dancer.png);
  animation-name: dance_in_place;
  animation-duration: 4s;
  animation-timing-function: steps(22, end);
  animation-iteration-count: infinite;
}

或许看起来像是复杂动画的东西实际上非常简单:就像在翻书中,我们一次只看到精灵的一个帧。我们的关键帧动画只是移动背景。

这样就覆盖了第一个参数,即步骤数。第二个参数接受几个值之一:step-startstartstep-endendjump-nonejump-both。给定的值指定第一个步骤间隔的变更是在给定间隔的开始还是结束时发生。(第十八章详细描述了这些值。)

使用默认值end或其等效值step-end,变更将发生在第一步的末尾。换句话说,给定 200 毫秒的步长,动画的第一个变更将在动画总时长的 200 毫秒后才发生。使用startstep-start,第一个变更将在第一个步骤间隔的开始处发生;也就是说,动画开始时立即发生变化。Figure 19-4 提供了基于以下样式的两个值如何工作的时间轴图示:

@keyframes grayfade {
    from {background-color: #BBB;}
    to {background-color: #333;}
}

.slowfader  {animation: grayfade 1s steps(5,end);}
.quickfader {animation: grayfade 1s steps(5,start);}

css5 1904

图 19-4。可视化开始和结束变更点

嵌入到每个时间轴中的方框表示该步骤间隔期间的背景颜色。请注意,在end时间轴上,第一个间隔与动画开始前的背景相同。这是因为动画等到第一帧结束后才进行第一步的颜色更改(即“Step 1”和“Step 2”之间的颜色)。

另一方面,在start时间轴上,第一个间隔使得颜色在间隔开始时发生变化,立即从起始背景颜色切换到“Step 1”和“Step 2”之间的颜色。这有点像提前跳过一个间隔,这种印象被这样一个事实加强:end时间轴上的“Step 2”背景颜色与start时间轴上的“Step 1”相同。

在每次动画结束时都可以看到类似的效果,其中第start时间轴的第五步背景与结束背景颜色相同。在end时间轴上,它是在“Step 4”和“Step 5”之间的点处的颜色,直到动画结束时才切换到结束背景颜色。

更改参数可能很难理清。如果有帮助的话,可以这样想:在正常的动画方向中,start值“跳过”了 0%关键帧,因为它在动画开始时就进行了第一个更改,而end值“跳过”了100%关键帧。

step-start值等于steps(1, start),只显示100%关键帧。step-end值等于steps(1, end),仅显示0%关键帧。

动画化时间函数

animation-timing-function 不是可动画属性,但可以包含在关键帧中以改变动画的当前时间。

与可动画属性不同,animation-timing-function 值不会随时间插值。在 @keyframes 定义中的关键帧中包含时,当达到该关键帧时,声明在同一关键帧内的属性的时间函数将更改为新的 animation-timing-function 值,如 图 19-5 所示:

.pencil {animation: W 3s infinite linear;}
@keyframes width {
  0% {
    width: 200px;
    animation-timing-function: linear;
  }
  50% {
    width: 350px;
    animation-timing-function: ease-in;
  }
  100% {
    width: 500px;
  }
}

在前面的例子中,如 图 19-5 所示,在动画的一半处,我们从 width 属性的线性动画进度切换到缓入动画。缓入时间函数从更改时间函数的关键帧开始。

to100% 关键帧中指定 animation-timing-function 将不会影响动画。在任何其他关键帧中包含时,动画将遵循该关键帧定义中指定的 animation-timing-function,直到达到下一个关键帧,覆盖元素的默认或声明的 animation-timing-function

css5 1905

图 19-5. 更改动画时间函数的中途动画

如果在关键帧中包含了 animation-timing-function 属性,则仅在该关键帧块中还包含该属性的属性才会受其时间函数的影响。新的时间函数将在该属性上起作用,直到达到下一个包含该属性的关键帧为止,此时它将更改为该块内声明的时间函数,或者恢复到分配给该元素的原始时间函数。以我们的 W 动画为例:

@keyframes W {
    from     { left: 0; top: 0; }
    25%, 75% { top: 100%; }
    50%      { top: 50%; }
    to       { left: 100%; top: 0; }
}

这遵循的概念是,概念上,当在元素或伪元素上设置动画时,就好像为每个出现在任何关键帧中的属性创建了一组关键帧,好像每个属性的动画都是独立运行的。就像 W 动画由两个同时运行的动画组成一样 — W_part1W_part2

@keyframes W_part1 {
    from, to { top: 0; }
    25%, 75% { top: 100%; }
    50%      { top: 50%; }
}
@keyframes W_part2 {
    from { left: 0; }
    to   { left: 100%; }
}

只有在任何关键帧中设置的 animation-timing-function 才会添加到仅在该关键帧定义的属性的进度中:

@keyframes W {
    from     { left: 0; top: 0; }
    25%, 75% { top: 100%; }
    50%      { animation-timing-function: ease-in; top: 50%; }
    to       { left: 100%; top: 0; }
}

前述代码将把 animation-timing-function 从 CSS 选择器块上设置的任何内容更改为仅对 top 属性为 ease-in,而不影响 left 属性,仅在动画的中间到 75% 的标记。

然而,对于以下动画,animation-timing-function 将没有效果,因为它被放置在一个没有属性值声明的关键帧块中:

@keyframes W {
    from     { left: 0; top: 0; }
    25%, 75% { top: 100%; }
    50%      { animation-timing-function: ease-in; }
    50%      { top: 50%; }
    to       { left: 100%; top: 0; }
}

在动画中间改变时间函数有什么用?在弹跳动画中,我们处于无摩擦的环境:球永远弹跳,永不失去动量。球下落时加速,上升时减速,因为默认情况下,随着动画从normalreverse方向的进行,时间函数从ease-in反转为ease-out。每隔一次迭代。

实际上,存在摩擦;动量会丢失。球不会无限地继续弹跳。如果我们希望我们的弹跳球看起来自然,我们必须使其在每次碰撞时能量减少而弹跳得不那么高。为此,我们需要一个单一动画,多次弹跳,每次弹跳时动量减少,同时在每个顶点和谷底之间在ease-inease-out之间切换:

@keyframes bounce {
  0% {
    transform: translateY(0);
    animation-timing-function: ease-in;
  }
  30% {
    transform: translateY(100px);
    animation-timing-function: ease-in;
  }
  58% {
    transform: translateY(200px);
    animation-timing-function: ease-in;
  }
  80% {
    transform: translateY(300px);
    animation-timing-function: ease-in;
  }
  95% {
    transform: translateY(360px);
    animation-timing-function: ease-in;
  }
  15%, 45%, 71%, 89%, 100% {
    transform: translateY(380px);
    animation-timing-function: ease-out;
  }
}

这个动画在几次弹跳后高度减少,最终停止。

由于这个新动画只使用一个迭代,我们不能依赖animation-direction来改变我们的时间函数。我们需要确保每次弹跳都会使球失去动量,但仍会受到重力加速并在达到顶点时减速。因为我们只有一个迭代,我们通过在关键帧中包含animation-timing-function来控制时间。在每个顶点处,我们切换到ease-in,在每个谷底或弹跳处,我们切换到ease-out

设置动画播放状态

如果需要暂停和恢复动画,animation-play-state 属性定义动画是运行还是暂停。

当设置为默认值running时,动画会正常进行。如果设置为paused,动画将被暂停。在paused状态下,动画仍然应用于元素,只是在暂停之前的进度上被冻结。如果在迭代过程中停止,正在进行动画的属性将保持在中间值。当设置回running时,动画将从暂停的地方重新开始,就好像控制动画的“时钟”已经停止并重新启动一样。

如果在动画的延迟阶段将属性设置为paused,则延迟时钟也会暂停,并在animation-play-state重新设置为运行时恢复。

动画填充模式

animation-fill-mode 属性使我们能够定义元素的属性值在动画持续时间之外是否继续应用。

此属性很有用,因为默认情况下,动画中的更改仅在动画本身期间应用。在动画开始之前,不会应用动画属性值。一旦动画完成,所有值将立即恢复为它们的动画前值。因此,如果您将背景从红色动画到蓝色,动画完成之前背景将保持为红色,并且在动画完成后立即恢复为红色。

类似地,如果应用了正的 animation-delay,动画将不会立即影响元素的属性值。相反,动画属性值将在 animation-delay 到期时,即在 animationstart 事件触发时应用。

使用 animation-fill-mode,我们可以定义动画在附加到元素之前和 animationend 事件触发后对元素产生的影响。在任何动画延迟到期期间,可以将 0%关键帧中设置的属性值应用于元素,并且这些属性值可以在 animationend 事件触发后继续存在。

animation-fill-mode 的默认值是 none,这意味着在动画未执行时动画没有任何效果。动画的 0%关键帧(或反向动画的 100%关键帧)的属性值在动画延迟到期并触发 animationstart 事件时才会应用于动画元素。

当值设置为 backwardsanimation-directionnormalalternate 时,将立即应用 0% 关键帧的属性值,无需等待 animation-delay 时间到期。如果 animation-directionreversedreversed-alternate,则将应用 100% 关键帧的属性值。

forwards 值意味着当动画执行完成——即按照 animation-iteration-count 值定义的最后迭代的最后部分,并且触发了 animationend 事件后——它将继续应用属性值,这些属性值是在 animationend 事件发生时的状态。如果 iteration-count 具有整数值,则此值将是 100% 关键帧,或者如果最后一个迭代是反向的,则是 0% 关键帧。

both 值同时应用 backwards 效果,即在动画附加到元素时立即应用属性值,并且 forwards 效果,即在 animationend 事件之后持续应用属性值。

如果animation-iteration-count是浮点值而不是整数,则最后一个迭代不会在0%100%关键帧结束;相反,动画将在动画周期的中途结束其执行。如果animation-fill-mode设置为forwardsboth,元素将保持在animationend事件发生时的属性值。例如,如果animation-iteration-count6.5,并且animation-timing-function为 linear,则animationend事件触发时,属性在 50%标记处的值将保持不变,就好像在该点设置了animation-play-statepause一样。

例如,考虑以下代码:

@keyframes move_me {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(1000px);
  }
}

.moved {
  transform: translateX(0);
  animation-name: move_me;
  animation-duration: 10s;
  animation-timing-function: linear;
  animation-iteration-count: 0.6;
  animation-fill-mode: forwards;
}

动画将只进行 0.6 次迭代。作为线性的 10 秒动画,它将在 60%的位置停止,即动画进行到第 6 秒时,元素向右移动 600 像素。当animation-fill-mode设置为forwardsboth时,动画在元素被移动到右侧 600 像素时停止动画,保持元素在其原始位置的右侧 600 像素处。这将使其永久地保持平移状态,至少在动画与元素分离之前是这样。如果没有animation-fill-mode: forwards,则具有moved类的元素将弹回其原始的transform: translateX(0),如在移动选择器代码块中定义的那样。

综合所有内容

animation简写属性允许您使用一个声明来定义元素动画的所有参数,而不是八个。animation属性值是各种长手动画属性的以空格分隔的值列表。如果您在元素或伪元素上设置多个动画,可以使用以逗号分隔的动画列表。

动画简写属性作为其值接受了所有其他先前的动画属性,包括animation-durationanimation-timing-functionanimation-delayanimation-iteration-countanimation-directionanimation-fill-modeanimation-play-stateanimation-name。例如,以下两个规则完全等效:

#animated {
  animation: 200ms ease-in 50ms 1 normal running forwards slidedown;
}
#animated {
  animation-name: slidedown;
  animation-duration: 200ms;
  animation-timing-function: ease-in;
  animation-delay: 50ms;
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
  animation-direction: normal;
  animation-play-state: running;
}

我们不必在动画简写中声明所有值;未声明的任何值都设置为默认或初始值。在前面的示例中,三个属性设置为它们的默认值,因此它们并不严格必要,尽管有时将它们写入作为未来参考(或接管代码维护的人)的提醒是个好主意。

简写的顺序在两个特定方面很重要。首先,允许两个时间属性,用于<animation-duration>和<animation-delay>。当列出两个时,第一个始终是持续时间。第二个(如果存在)被解释为延迟。

其次,animation-name的放置位置也很重要。如果将动画属性值用作动画名称(虽然不推荐,但假设你这样做了),则animation-name应放置在animation速记中的最后一个属性值。关键字的第一次出现,如果它是其他任何动画属性的有效值(如easerunning),则假定它是速记动画属性的一部分,而不是animation-name。以下规则是等效的:

#failedAnimation {
    animation: paused 2s;
}

#failedAnimation {
    animation-name: none;
    animation-duration: 2s;
    animation-delay: 0;
    animation-timing-function: ease;
    animation-iteration-count: 1;
    animation-fill-mode: none;
    animation-direction: normal;
    animation-play-state: paused;
}

这是因为paused是一个有效的动画名称。尽管看起来似乎将持续时间为2s的名为paused的动画附加到元素上,但实际情况并非如此。因为在速记动画中,除animation-name之外的所有动画属性的可能有效值都首先与单词进行比较,所以paused被设置为animation-play-state属性的值。因为没有可识别的动画名称,所以animation-name的值保持默认值none

这里有一个你不应该做的例子:

#anotherFailedAnimation {
    animation: running 2s ease-in-out forwards;
}

#anotherFailedAnimation {
    animation-name: none;
    animation-duration: 2s;
    animation-delay: 0s;
    animation-timing-function: ease-in-out;
    animation-iteration-count: 1;
    animation-fill-mode: forwards;
    animation-direction: normal;
    animation-play-state: running;
}

在这里,作者可能有一个名为running的关键帧动画。然而,浏览器看到这个术语,并将其分配给animation-play-state属性,而不是animation-name属性。因为没有声明animation-name,所以没有动画附加到元素上。

解决这个问题的方法如下所示:

#aSuccessfulIfInadvisableAnimation {
    animation: running 2s ease-in-out forwards running;
}

这将将第一个running应用于animation-play-state,将第二个running应用于animation-name。再次强调:建议这样做。这样做可能导致混淆和错误。

根据这一切,animation: 2s 3s 4s;可能看起来是有效的,就好像正在设置以下内容:

#invalidName {
    animation-name: 4s;
    animation-duration: 2s;
    animation-delay: 3s;
}

但正如在“设置关键帧动画”中提到的,4s是一个有效的标识符。除非转义,标识符不能以数字开头。为了使此动画有效,必须写成animation: 2s 3s \4s;

要将多个动画附加到单个元素或伪元素上,请用逗号分隔动画:

.snowflake {
  animation: 3s ease-in 200ms 32 forwards falling,
             1.5s linear 200ms 64 spinning;
}

每个雪花将在旋转 96 秒的同时落下,每次 3 秒的下落中旋转两次。 在最后一个动画周期结束时,雪花将停留在falling动画的100%关键帧上。我们为falling动画声明了八个动画属性中的六个,并为旋转动画声明了五个,用逗号分隔了这两个动画。

虽然你经常会看到动画名称作为第一个值——这样更容易阅读,因为动画属性关键字作为有效的关键帧标识符可能会导致问题,但这并不是最佳实践。这就是为什么我们把动画名称放在最后的原因。

总之:使用animation简写是个好主意。只需记住,在该简写中,持续时间、延迟和名称的位置很重要,省略的值将被设置为它们的默认值。

还要注意,虽然none基本上是唯一不能作为有效动画名称的词,但使用任何动画关键词作为您的标识符从来不是一个好主意。

动画、特异性和优先级顺序

就特异性、级联以及哪些属性值应用于元素而言,动画优先于级联中的所有其他值。

特异性和!important

一般来说,使用 ID 选择器1-0-0附加的属性权重应优先于通过元素选择器0-0-1应用的属性。但是,如果通过关键帧动画更改了该属性值,新值将被应用,就好像该属性值对被添加为内联样式并覆盖了先前的值一样。

动画规范说明:“动画覆盖所有普通规则,但会被!important规则覆盖。”因此,在动画声明块内不要添加!important到属性,这样做是无效的,被添加了!important的属性-值组合将被忽略。

动画迭代和显示:none;

如果在元素上将display属性设置为none,则在该元素或其后代上迭代的任何动画都将停止,就好像动画从元素上解除绑定一样。将display属性更新回可见值将重新附加所有动画属性,从头开始重新启动动画:

.snowflake {
  animation: spin 2s linear 5s 20;
}

在这种情况下,雪花将旋转 20 次;每次旋转需要 2 秒钟,第一次旋转在 5 秒后开始。如果雪花元素的display属性在 15 秒后被设置为none,它将在消失之前完成 5 次旋转(在经过 5 秒延迟后执行 5 次每次 2 秒的旋转)。如果雪花的display属性再次改为除了none之外的任何其他值,则动画将从头开始:再次等待 5 秒延迟,然后开始旋转 20 次。无论第一次从视图中消失之前迭代了多少动画周期,都不会有任何影响。

动画和 UI 线程

CSS 动画在 UI 线程上具有最低优先级。如果在页面加载时附加了多个带有正值animation-delay的动画,延迟将如指定的那样到期,但动画可能要等到 UI 线程可用于动画时才会开始。

假设以下情况:

  • 所有动画都需要 UI 线程(也就是说,它们不在 GPU 上,如“动画链”所述)。

  • 您有 20 个动画,每个动画的animation-delay属性设置为1s2s3s4s等,以便依次在前一个动画后延迟 1 秒启动每个后续动画。

  • 文档或应用程序加载时间很长,从绘制动画元素到页面上的时间开始,到 JavaScript 完成下载、解析和执行,整整 11 秒钟。

给定所有这些,当 UI 线程可用时,前 11 个动画的延迟将会过期,并且这些前 11 个动画将同时开始。每个剩余的动画然后将以 1 秒间隔开始动画。

使用 will-change 属性

您可以创建如此复杂的动画,以至于它们呈现得很差,会出现卡顿或所谓的 jank。在这种情况下,告诉浏览器提前通过 will-change 属性动画化需求可能会有所帮助。

这里的基本思想是向浏览器提供关于可能需要昂贵计算的预优化提示。

警告

只有在通过其他方法(例如以微妙但显著的方式简化动画)无法解决动画问题,并且您认为预优化会解决问题时,才应使用 will-change。如果尝试使用 will-change 并且没有看到值得的改善,应将其移除而不是留在原位。

默认值 auto 将优化工作留给浏览器,与通常一样。scroll-position 值表示预期文档滚动位置的动画,或至少会发生一些变化。默认情况下,浏览器通常仅考虑视口的内容及其两侧的少量内容。scroll-position 值可能会导致浏览器在其布局计算中引入更多视口两侧的内容。尽管这可能会产生更平滑的滚动动画,但扩展的范围可能会显著减慢视口内可见内容的渲染速度。

使用 contents,告诉浏览器预期元素内容的动画。这最有可能导致浏览器减少或消除视口内容的缓存。这将要求浏览器每帧从头开始重新计算页面布局。不断重新计算页面布局可能会导致页面的渲染速度低于每秒 60 帧,这通常是浏览器制造商试图达到的基准。另一方面,如果内容将被频繁更改和动画化,告诉浏览器减少缓存可能是有道理的。再次强调,只有在已经知道动画对浏览器负荷过重时才应尝试这样做——切勿提前假设。

还可以通过使用 <custom-ident> 告诉浏览器要注意的属性,这在这种情况下是指“属性”。例如,如果您有一个复杂的动画集,会改变位置、滤镜和文本阴影,并且它们被证明速度慢或有卡顿现象,您可以尝试这样做:

will-change: top, left, filter, text-shadow;

如果这使动画变得平滑,逐个移除属性以查看平滑度是否保持是值得的。例如,你可能会发现移除topleft属性并不影响新的平滑度,但移除filtertext-shadow会导致卡顿问题再次出现。在这种情况下,将其保留为will-change: filter, text-shadow

还要记住,像fontbackground这样列出简写属性会导致所有长手属性被视为可更改。因此,以下两条规则是等效的:

.textAn {will-change: font;}

.textAn {will-change: font-family, font-size, font-weight, font-style,
     font-variant, line-height;}

这就是为什么在几乎任何情况下,简写属性不应列在will-change中的原因。而是要识别正在进行动画的长手属性,并将其列出。

打印动画

当打印动画元素时,应打印其最终状态。你无法在纸上看到元素动画;但是,例如,如果动画导致元素具有border-radius50%,打印的元素将具有border-radius50%

总结

正如我们希望本章所展示的那样,动画可以成为用户界面以及设计的装饰部分的强大补充。无论动画是简单的、复杂的、短暂的还是长久的,所有这些方面都掌握在你的手中。

总是要谨慎行事,因为动画可能会对一些用户产生负面影响,无论他们是否患有前庭障碍或仅对运动敏感。幸运的是,prefers-reduced-motion可以减少或消除不希望动画的用户的动画。

第二十章:滤镜、混合、裁剪和遮罩

几个特殊属性允许作者通过视觉滤镜改变元素的外观,指定不同的方式将元素视觉混合到其后的内容中,并通过显示部分和隐藏其他部分来改变元素的呈现方式。虽然这些可能看起来是不同的概念,但它们都有一个共同点:它们允许以前难以或不可能的方式改变元素。

CSS 滤镜

CSS 提供了一种通过 filter 属性对元素应用内置视觉滤镜效果以及在页面或外部文件中定义的自定义滤镜的方法。

值语法允许空格分隔的滤镜函数列表,每个滤镜依次应用。因此,给定声明 filter: opacity(0.5) blur(1px);,透明度应用于元素,然后模糊这个半透明的结果。如果顺序颠倒,应用的顺序也会颠倒:完全不透明的元素被模糊,然后再使模糊后的结果变得半透明。

CSS 规范在讨论 filter 时提到了“输入图像”,但这并不意味着 filter 只能用在图像上。任何 HTML 元素都可以被过滤,并且所有图形 SVG 元素也可以被过滤。输入图像 是渲染元素被过滤的视觉副本。过滤器应用于这个输入,最终过滤后的结果再渲染到显示介质(例如设备显示器)上。

允许的所有值(除了 url())都是函数值,每个函数的允许值类型取决于具体的函数。为了便于理解,我们将这些函数分成了几个广泛的类别。

基本滤镜

以下滤镜在直接描述它们名称变化的意义上是基本的:模糊、投影阴影和透明度变化:

blur( <length> )

通过使用由提供的 <length> 值定义的高斯模糊来模糊元素的内容,其中值为 0 保持元素不变。不允许负值长度。

opacity( [ <number> | <percentage> ] )

以一种非常类似于 opacity 属性的方式,向元素应用透明度滤镜,其中值 0 会使元素完全透明,值为 1100% 保持元素不变。不允许负值。允许大于 1100% 的值,但在计算最终值时会被剪切为 1100%

警告

规范明确指出 filter: opacity() opacity 属性的替代或简写,事实上两者都可以应用于同一元素,从而导致一种双重透明效果。

drop-shadow( <length>{2,3} <color>? )

创建一个与元素的 alpha 通道形状匹配的模糊阴影,并使用可选颜色。长度和颜色的处理与 box-shadow 属性相同,这意味着前两个 <length> 值可以是负值,但第三个(定义模糊度)不能是负值。不过与 box-shadow 不同的是,这里不允许 inset 值。要应用多个阴影,提供多个以空格分隔的 drop-shadow() 函数;不像 box-shadow,逗号分隔的阴影在这里不起作用。如果没有提供 <color> 值,则使用的颜色与元素的 color 属性的计算值相同。

图 20-1 展示了这些 filter 函数的一些效果。

css5 2001

图 20-1. 基本滤镜效果

在继续之前,有两件事值得进一步探讨。首先是 drop-shadow() 的实际操作方式。仅仅通过查看图 20-1,很容易得出 drop-shadow 是与元素框绑定的结论,因为所示的阴影具有盒状的特性。但这只是因为用来说明滤镜的图像是一个 PNG 图像,也就是一种光栅图像,更重要的是这种图像没有任何 alpha 通道。图像的白色部分是不透明的白色,换句话说。

如果图像具有透明部分,drop-shadow() 将使用这些部分来计算阴影。要了解这意味着什么,请考虑图 20-2。

css5 2002

图 20-2. 阴影和 alpha 通道

还有一点要指出在图 20-2 中,最后一张图片有两个阴影。这是通过以下方式完成的:

filter: drop-shadow(0 0 0.5em yellow) drop-shadow(0.5em 0.75em 30px gray);

可以像这样链接任意数量的过滤器。举个例子,你可以写如下内容:

filter: blur(3px) drop-shadow(0.5em 0.75em 30px gray) opacity(0.5);

这将使您得到一个模糊的、带阴影的、半透明的元素。对于文本来说,这可能不是最友好的效果,但它仍然是可能的。所有 filter 函数都支持此类函数链接。

颜色过滤

这组 filter 函数会改变元素中的颜色。这可以简单到淡化颜色,也可以复杂到通过角度值移动所有颜色。

注意,对于接受 <number> 或 <percentage> 的以下四个函数中的前三个函数,不允许负值;第四个允许正负角度值:

grayscale( [ <number> | <percentage> ] )

将元素中的颜色调整为朝向灰度阴影。值为 0 保持元素不变,值为 1100% 将结果显示为黑白,即完全灰度元素。

sepia( [ <number> | <percentage> ] )

改变元素中的颜色,使其向赭色调的色调移动(赭色是古老摄影中使用的红棕色,由维基百科定义为在 sRGB 颜色空间中等于#704214rgba(112,66,20))。值为0保持元素不变,值为1100%将导致完全赭色的元素。

invert( [ <数字> | <百分比> ] )

反转元素中的所有颜色。给定颜色的每个 R、G 和 B 值都通过从 255(在 0-255 表示法中)或从 100%(在 0%-100%表示法中)中减去它们来进行反转。例如,具有颜色rgb(255 128 55)的像素将呈现为rgb(0 127 200);具有值为rgb(75% 57.2% 23%)的不同像素将变为rgb(25% 42.8% 77%)。值为0保持元素不变,值为1100%会导致完全反转的元素。值为0.550%将使每种颜色在颜色空间的中点停止反转,从而导致一个统一灰色的元素,而不考虑输入元素的外观。

hue-rotate( <角度> )

通过在 HSL 色轮上围绕色相角度改变图像的颜色,保持饱和度和亮度不变。值为0deg意味着输入和输出图像之间没有差异。值为360deg(完整的单次旋转)也将呈现看似未改变的元素,尽管旋转角度值被保留。允许使用大于360deg的值。也允许负值,负值导致逆时针旋转,而不是正值导致的顺时针旋转(换句话说,旋转是“罗盘式”的,0°在顶部,顺时针方向的角度值递增)。

前述filter函数的示例显示在图 20-3,尽管充分理解它们取决于颜色渲染。

css5 2003

图 20-3. 颜色滤镜效果

亮度、对比度和饱和度

虽然以下filter函数也操纵颜色,但它们以密切相关的方式执行此操作,并且对于那些曾处理过图像,特别是摄影图像的人来说,这些函数是熟悉的组合。对于所有这些函数,允许的值大于1100%,但在计算最终值时会被剪切为1100%

brightness( [ <数字> | <百分比> ] )

改变元素颜色的亮度。值为0使元素呈现纯黑色,值为1100%使其保持不变。大于1100%的值产生比输入元素更亮的颜色,并最终可能达到纯白色状态。

contrast( [ <数字> | <百分比> ] )

改变元素颜色的对比度。对比度越高,颜色之间的区别越大;对比度越低,颜色越接近。值为0会使元素呈现为纯灰色,值为1100%则保持不变。大于1100%的值会产生比输入元素更高对比度的颜色。

saturate( [ <number> | <percentage> ] )

改变元素颜色的饱和度。颜色的饱和度越高,颜色变得越强烈;饱和度越低,颜色变得越柔和。值为0会使元素完全不饱和,实际上呈现为灰度图像;而值为1100%则保持不变。类似于brightness()saturate()允许并作用于大于1100%的值;这些值会导致超饱和的效果。

显示了前述filter函数的示例,详细理解这些函数取决于颜色渲染。大于一的值的效果在图中可能难以辨认,但它们确实存在。

css5 2004

图 20-4. 亮度、对比度和饱和度滤镜效果

SVG 滤镜

最后一种filter值类型是熟悉类型的函数:url()值类型。这允许您指向 SVG 中定义的(可能非常复杂的)滤镜,无论它是嵌入在文档中还是存储在外部文件中。

形式为url(<*uri*>),其中<uri>指向使用 SVG 语法定义的滤镜,特别是<filter>元素。这可以是指向仅包含滤镜的单个 SVG 图像的引用,如url(wavy.svg),或者是指向 SVG 图像中标识滤镜的引用,如url(filters.svg#wavy)。后一种模式的优点是单个 SVG 文件可以定义多个滤镜,从而将所有滤镜整合到一个文件中,方便加载、缓存和引用。

如果url()函数指向不存在的文件,或者指向非<filter>元素的 SVG 片段,则该函数无效,整个函数列表将被忽略(因此filter声明无效)。

在 SVG 中检查所有滤镜可能性远超出本书的范围,但我们可以简单说一下提供的功能非常强大。在图 20-5 中展示了几个简单的 SVG 滤镜示例,简要说明了这些滤镜被设计用来创建的操作类型(应用这些滤镜的实际 CSS 看起来像filter: url(filters.svg#rough))。

css5 2005

图 20-5. SVG 滤镜效果

完全可以将您进行的所有过滤工作放入 SVG 中,包括您看到的其他filter函数的替换。 (事实上,所有其他filter函数在规范中被定义为文字 SVG 过滤器,以为实现者提供精确的渲染目标。)然而,请记住,您可以将 CSS 函数链接在一起。 因此,您可以在 SVG 中定义一个镜面高亮过滤器,并根据需要进行模糊处理或灰度处理。 例如:

img.logo {filter: url(/assets/filters.svg#spotlight);}
img.logo.print {filter: url(/assets/filters.svg#spotlight) grayscale(100%);}
img.logo.censored {filter: url(/assets/filters.svg#spotlight) blur(3px);}

请始终记住,过滤函数是按顺序应用的。 这就是为什么grayscale()blur()函数在url()导入的聚光灯过滤器之后使用。 如果反过来,徽标首先变为灰度或模糊,然后再应用聚光灯过滤器。

合成和混合

除了过滤外,CSS 还可以确定如何将元素合成在一起。例如,由于定位而部分重叠的两个元素。默认情况下,完全不透明的前置元素会完全遮挡其后的元素在重叠处。如果前置元素是半透明的,则后置元素部分可见。

有时这被称为简单的α合成,因为只要某些(或全部)元素具有小于1的α通道值,您就可以看到元素背后的内容。 想象一下,通过opacity: 0.5可以看到背景,或者 PNG 或 GIF 中设置为透明的区域。 这就是简单的α合成。

但是,如果您熟悉像 Photoshop 或 GIMP 这样的图像编辑程序,您就会知道重叠的图像层可以以多种方式混合在一起。 CSS 也具有相同的能力。 CSS 具有两种混合策略(至少在 2022 年末是这样):将整个元素与其背后的内容混合在一起,以及混合单个元素的背景层。 尽管在许多方面类似于过滤效果,但混合模式的值是预定义的 - 它们不接受参数 - 而支持混合模式的属性支持多个值,这些属性使用逗号分隔的值列表而不是空格分隔的值列表。 (这种值语法的不一致性深植于 CSS 的历史中,目前我们只能接受这种情况。)

混合元素

如果元素重叠,可以通过使用mix-blend-mode属性改变它们混合在一起的方式。

CSS 规范指出,此属性“定义了必须使用的公式来将颜色与背景混合在一起”。 元素与其背后的任何东西(“背景”)混合在一起,无论是另一个元素的片段还是诸如<body>之类的祖先元素的背景。

默认值 normal 将元素的像素显示为原样,与背景没有任何混合,除非 alpha 通道小于 1。这是前面提到的简单 alpha 合成。这是我们所习惯的,也是默认值的原因。图 20-6 展示了一些示例。

css5 2006

图 20-6. 简单的 alpha 通道混合

对于剩余的 mix-blend-mode 关键字,我们将它们分成几类。让我们还确定一下在混合模式描述中将要使用的一些定义:

前景

应用 mix-blend-mode 的元素。

背景

一个元素的背景。这可以是其他元素、祖先元素的背景等。

像素分量

给定像素的颜色分量:R、G 和 B。

如果有帮助的话,可以将前景和背景视为图像编辑程序中彼此叠加的图层。使用 mix-blend-mode,您可以更改应用于顶部元素(前景)的混合模式。

Darken、lighten、difference 和 exclusion

下面的混合模式可能被称为简单数学模式,它们通过直接比较值的方式或使用简单的加减来修改像素来实现其效果:

darken

前景中的每个像素与背景中对应的像素进行比较,对于 R、G 和 B 值(像素分量),保留两者中较小的值。因此,如果前景像素的值对应 rgb(91 164 22),背景像素是 rgb(102 104 255),则生成的像素将是 rgb(91 104 22)

lighten

这种混合是 darken 的反转:比较前景像素及其对应背景像素的 R、G 和 B 组件时,保留两者中较大的值。因此,如果前景像素的值对应 rgb(91 164 22),背景像素是 rgb(102 104 255),则生成的像素将是 rgb(102 164 255)

difference

前景中每个像素的 R、G 和 B 分量与背景中对应像素进行比较,它们之间相减的绝对值是最终结果。因此,如果前景像素的值对应 rgb(91 164 22),背景像素是 rgb(102 104 255),则生成的像素将是 rgb(11 60 233)。如果其中一个像素是白色,生成的像素将是非白色像素的反色。如果其中一个像素是黑色,则结果将与非黑色像素完全相同。

exclusion

这种混合是difference的一个较温和版本。而不是| 背景前景,公式是 背景 + 前景 – (2 × 背景 × 前景),其中背景前景的值在 0 到 1 的范围内。例如,橙色 (rgb(100% 50% 0%)) 和中灰色 (rgb(50% 50% 50%)) 的排除计算将得到 rgb(50% 50% 50%)。例如,绿色分量的计算为 0.5 + 0.5 – (2 × 0.5 × 0.5),结果为 0.5,对应 50%。与difference相比,其结果为 rgb(50% 0% 50%),因为每个分量是相减后的绝对值。

这最后的定义强调,对于所有混合模式,实际操作的值都在 0 到 1 的范围内。前面展示的 rgb(11 60 233) 等值是从 0 到 1 范围归一化的。换句话说,以应用difference混合模式到 rgb(91 164 22)rgb(102 104 255) 为例,实际操作如下:

  1. rgb(91 164 22)R = 91 ÷ 255 = 0.357;G = 164 ÷ 255 = 0.643;B = 22 ÷ 255 = 0.086。类似地,rgb(102 104 255) 对应于 R = 0.4;G = 0.408;B = 1。

  2. 每个分量从对应的分量中减去,并取绝对值。因此,R = | 0.357 – 0.4 | = 0.043;G = | 0.643 – 0.408 | = 0.235;B = | 1 – 0.086 | = 0.914。这可以表示为 rgba(4.3% 23.5% 91.4%),或者(通过将每个分量乘以 255)为 rgb(11 60 233)

由此可见,你可能理解为何我们不会为每种混合模式详细列出完整的公式。如果你对细节感兴趣,每种混合模式的公式都在“合成和混合级别 2”规范中提供。

图 20-7 展示了本节中混合模式的示例。

css5 2007

图 20-7。使用 mix-blend-mode: 应用于前景图像的暗化、亮化、差异和排除混合

Multiply、screen 和 overlay

下列混合模式可能被称为乘法模式——它们通过将值相乘来实现其效果:

multiply

前景中的每个像素分量与背景中的对应像素分量相乘。这会产生前景的较暗版本,通过底下的内容进行修改。这种混合模式是对称的,即结果即使将前景与背景互换也会完全相同。

screen

前景中的每个像素分量被反转(见“颜色过滤”中的invert),乘以背景中对应像素分量的倒数,然后再次反转。这会产生前景的较轻版本,通过底下的内容进行修改。像multiply一样,screen是对称的。

overlay

这种混合是multiplyscreen的结合体。对于比 0.5(50%)更暗的前景像素组件,执行multiply操作;对于值高于 0.5 的前景像素组件,使用screen。这使得暗区更暗,亮区更亮。这种混合模式对称,因为交换前景和背景会产生不同的光和暗的模式,进而产生不同的 multiply 和 screen 模式。

图 20-8 描述了这些混合模式的示例。

css5 2008

图 20-8. 使用mix-blend-mode属性显示 multiply、screen 和 overlay 混合的图像

硬光和软光

这里介绍以下混合模式,因为第一个与之前的混合模式密切相关,而第二个只是第一个的柔和版本:

hard-light

这种混合是overlay混合的反转。像overlay一样,它是multiplyscreen的结合,但决定层是背景。因此,对于背景像素组件低于 0.5(50%)的部分,执行multiply操作;对于背景像素组件高于 0.5 的部分,使用screen。这使得前景看起来像是被一个使用强烈光线的投影仪投射到背景上。

soft-light

这种混合是hard-light的柔和版本。此模式使用相同的操作,但效果更柔和。其预期外观是,前景像是被一个使用散射光的投影仪投射到背景上。

图 20-9 描述了这些混合模式的示例。

css5 2009

图 20-9. 硬光和软光混合

颜色减淡和加深

颜色减淡和加深——这些术语来自于旧的在化学胶片上进行的暗房技术,用于尽可能少地改变颜色本身来调整图片的明暗。这些模式包括:

color-dodge

前景中的每个像素组件都被反转,相应背景像素组件的值被反转的前景值除以。这会使得背景明亮,除非前景值为0,此时背景值保持不变。

color-burn

这种混合是color-dodge的反转:背景中的每个像素组件都被反转,反转后的背景值被对应前景像素组件的不变值除以,然后结果再反转。这会导致背景像素越暗,其颜色就越能穿透前景像素。

图 20-10 描述了这些混合模式的示例。

css5 2010

图 20-10. 使用mix-blend-mode: color-dodgemix-blend-mode: color-burn 进行混合

色相、饱和度、亮度和颜色

最后四种混合模式与我们之前展示的不同,因为它们不对 R/G/B 像素分量执行操作。相反,它们以不同的方式组合前景和背景的色调、饱和度、亮度和颜色。这些模式如下:

色调

对于每个像素,将背景的亮度和饱和度水平与前景的色调角结合起来。

饱和度

对于每个像素,将背景的色调角和亮度级别与前景的饱和度水平结合起来。

颜色

对于每个像素,将背景的亮度级别与前景的色调角和饱和度水平结合起来。

亮度

对于每个像素,将背景的色调角和饱和度水平与前景的亮度级别结合起来。

图 20-11 展示了这些混合模式的示例。

css5 2011

图 20-11. 色调、饱和度、亮度和颜色混合

如果你不熟悉饱和度和亮度水平的确定方式,那么这些混合模式可能会更难理解,即使使用原始公式也可能会令人困惑。如果你觉得自己对这些模式的工作方式还不是很掌握,最好的解决办法是练习使用大量的图像和简单的色彩模式。

有两点需要注意:

  • 记住,元素始终与其背景混合。如果在元素后面有其他元素,它将与它们混合;如果父元素有图案背景,混合将针对该图案进行。

  • 更改混合元素的不透明度将改变结果,尽管结果可能并非总是你所期望的。例如,如果具有mix-blend-mode: difference的元素也设置了opacity: 0.8,则差异计算将按 80%缩放。具体而言,将色值计算应用了 0.8 的缩放因子。这可能导致某些操作趋向于平坦的中间灰色,而其他操作则会改变颜色变化。

混合背景

将元素与其背景混合是一回事,但如果一个元素有多个重叠的背景图片需要混合在一起,那该怎么办?这就是background-blend-mode发挥作用的地方。

我们不会详尽列出所有混合模式及其含义,因为我们在“混合元素”中已经这样做过。它们在那里的含义,这里也一样。

区别在于,当涉及混合多个背景图片时,它们将与空背景(完全透明、无色背景)混合在一起。它们不会与元素的背景混合,除非由mix-blend-mode指定。要看看这意味着什么,考虑以下情况:

#example {background-image:
        url(star.svg),
        url(diamond.png),
        linear-gradient(135deg, #F00, #AEA);
    background-blend-mode: color-burn, luminosity, darken;}

这里我们有三个背景图片,每个都有自己的混合模式。它们被混合在一起,显示在图 20-12 中的单个结果中。

css5 2012

图 20-12. 三个背景混合在一起

到目前为止,一切都很好。这里的关键是:无论出现在元素背后的是什么,结果都会相同。我们可以将父元素的背景更改为白色、灰色、紫红色或重复梯度的可爱图案,但这三个混合的背景看起来都会完全相同,像素对像素。它们被隔离混合,这是我们马上会回头讨论的一个术语。我们可以看到前面的例子 (图 20-12) 位于 图 20-13 中各种背景之上。

css5 2013

图 20-13. 使用颜色与透明度混合

就像多个混合元素堆叠在一起一样,背景层的混合从后到前进行。因此,如果您在一个纯色背景上有两个背景图像,则将背景图像的后置层与背景色混合,然后将前置层与第一个混合的结果混合。请考虑以下情况:

.bbm {background-image:
        url(star.svg),
        url(diamond.png);
    background-color: goldenrod;
    background-blend-mode: color-burn, luminosity;}

有了这些样式,diamond.png 使用luminosity混合模式与背景色goldenrod混合。一旦完成这个步骤,star.svg 将使用color-burn混合模式与 diamond-goldenrod 混合结果混合。

尽管背景层确实是在隔离状态下混合的,但它们也是元素的一部分,可能通过mix-blend-mode具有自己的混合规则。因此,隔离背景混合的最终结果可能会在某种程度上与元素的背景混合。有了以下样式,第一个例子的背景将位于元素的背景之上,但其余的混合将以某种方式与之混合,正如 图 20-14 中所示:

.one {mix-blend-mode: normal;}
.two {mix-blend-mode: multiply;}
.three {mix-blend-mode: darken;}
.four {mix-blend-mode: luminosity;}
.five {mix-blend-mode: color-dodge;}
<div class="bbm one"></div>
<div class="bbm two"></div>
<div class="bbm three"></div>
<div class="bbm four"></div>
<div class="bbm five"></div>

css5 2014

图 20-14. 元素与它们的背景混合

本节中,我们提到了隔离混合的概念,这是背景元素自然发生的事情。然而,元素本身并不会在隔离状态下自然混合。接下来您会看到,这种行为是可以改变的。

隔离混合

有时候您可能想要将多个元素一起混合,但是在它们自己的组中,就像元素的背景层一样被混合。正如您所见,这被称为隔离混合。如果这正是您想要的,那么isolation属性正是为您量身定制的。

这几乎完全按照其字面意思执行:它定义了一个元素是否创建一个隔离的混合上下文。有了以下样式,我们得到了 图 20-15 中显示的结果:

img {mix-blend-mode: difference;}
p.alone {isolation: isolate;}
<p class="alone"><img src="diamond.png"></p>
<p><img src="diamond.png"></p>

css5 2015

图 20-15. 隔离混合与非隔离混合

特别注意isolation的应用位置以及mix-blend-mode的应用位置。图像给出了混合模式,但包含元素(在本例中为段落)设置为隔离混合。这样做是因为您希望父元素(或祖先元素)在其后代元素的混合方面与文档的其他部分隔离开来。因此,如果要使元素在隔离中混合,找到一个祖先元素并设置其isolation: isolate

在所有这些情况中,都会出现一个有趣的问题:任何形成堆叠上下文的元素都会自动隔离,而不管isolation值如何。例如,如果使用transform属性转换元素,则它将变得孤立。

自 2022 年末起,形成堆叠上下文的完整条件列表如下:

  • 根元素(例如,<html>

  • 将元素设置为 flex 或 grid 项目,并将其z-index设置为除auto以外的任何值。

  • 使用relativeabsolute定位元素,并将其z-index设置为除auto以外的任何值。

  • 使用fixedsticky定位元素,不论其z-index值如何。

  • opacity设置为除1以外的任何值。

  • transform设置为除none以外的任何值。

  • mix-blend-mode设置为除normal以外的任何值。

  • filter设置为除none以外的任何值。

  • perspective设置为除none以外的任何值。

  • mask-imagemask-bordermask设置为除none以外的任何值。

  • isolation设置为isolate

  • contain设置为包含layoutpaint的值。

  • will-change应用于任何其他属性,即使实际上没有改变。

因此,如果您有一组元素进行混合,然后与它们的共享背景混合,然后将组的opacity1过渡到0,在过渡期间该组将突然变得孤立。这可能根据原始混合集合没有视觉影响,但也可能有。

包含元素。

类似于为混合模式而隔离元素,CSS 有一个称为contain的属性,用于限制元素布局如何受其他内容影响,以及其布局会如何影响其他内容。它旨在为作者提供给浏览器的优化提示。

默认情况下,none表示不指示任何包含,因此也不提供任何优化提示。每个其他值都有其自己的独特效果,因此我们将依次检查它们。

或许四种替代方案中最简单的是 contain: paint。设置此值时,元素的绘制被限制在其溢出框内,因此任何后代元素不能在该区域外绘制。这在很多方面类似于 overflow: hidden。不同之处在于启用绘制约束后,将永远无法显示元素及其后代未绘制的部分;因此,没有滚动条、点击拖动或其他用户操作能使未绘制内容显示出来。这允许浏览器完全忽略屏幕外或其他方式不可见元素的布局和绘制,因为其后代也无法显示。

在复杂度上更进一步的选项是 contain: style。使用 style 值时,诸如计数器递增和重置以及引号嵌套等样式在包含元素内被计算时,就好像在外部不存在这些样式一样,并且它们不能离开元素影响其他元素。这听起来像是创建了作用域样式,可以让一组样式仅适用于 DOM 的子树,但实际上并非如此。它仅对计数器和引号嵌套等内容有效。

更具影响力的选择是 contain: size。该值使得元素在布局时不会检查其后代元素可能如何影响其布局,并且其大小被计算为没有后代元素的情况,这意味着它的高度为零。它也被视为没有固有的宽高比,即使元素是 <img><svg>、表单输入或其他通常具有固有宽高比的内容。

这里有几个大小约束的例子,见 图 20-16:

p {contain: size; border: medium solid gray; padding: 1px;}
figure img {contain: size; border: 1px solid; width: 300px;}
<p>This is a paragraph.</p>

<figure>
   <img src="i/bigimg.gif">
   <figcaption>That’s a big image.</figcaption>
</figure>

图 20-16. 大小约束示例

或许这很有趣,但是它有用吗?举一个例子,当 JavaScript 用于基于祖先元素大小而不是相反的方式(容器查询)来调整元素大小时,可以防止布局循环。这也可以应用于已知在页面渲染时在屏幕外的元素,以最小化浏览器所需的工作量。

最后一种约束类型由 contain: layout 触发。这允许片段进入其中,但不允许任何片段逃逸,就像 CSS Regions 等提议功能可能会做的那样。设置 layout 后,元素的内部布局与页面的其余部分隔离开来。这意味着元素内部的任何内容都不会影响元素外部的任何内容,反之亦然。

可以在单个规则中使用多个关键字,例如 contain: size paint。这导致了最后两个可能的关键字,contentstrictcontent 关键字是 layout paint style 的简写,而 strictsize layout paint style 的简写。换句话说,content 包含除了尺寸外的所有内容,而 strict 则以所有可能的方式包含。

一个重要的警告是,contain 可以应用于以下元素,但有以下例外:不生成框的元素(例如 display: nonedisplay: contents),内部表格框不是表格单元格,内部 Ruby 框和非原子性内联级框不能设置为 paintsizelayout。此外,具有内部显示类型为 table(例如 <table>)的元素不能设置为 size。任何元素都可以设置为 style

我们还有一个需要注意的地方:即使没有 contain,某些形式的包含也可以被调用。例如,overflow: hidden 将有效地产生与 contain: paint 相同的结果,即使 contain: none 可能也适用于同一元素。

所有这些导致我们到达另一个包含属性,content-visibility,它有效地调用包含的种类,以及潜在地抑制元素内容的呈现。

在默认情况下,visible,元素的内容会正常显示。

如果使用 hidden 值,则元素的所有内容都不会被呈现,并且它们不参与元素的尺寸调整,就好像所有内容(包括任何超出后代元素的文本)都已设置为 display: none。此外,被抑制的内容不应该对页面搜索和标签顺序导航等产生影响,并且不可选择(例如鼠标点击和拖动)或可聚焦。

如果使用 auto,则启用绘制、样式和布局的包含,就好像声明了 contain: content。内容可能会被用户代理跳过,也可能不会;如果元素不在屏幕上或以其他方式不可见,最有可能会跳过。在这种情况下的内容 供页面搜索和标签顺序导航,并且可以选择和聚焦。

警告

截至 2023 年初,content-visibility 在 Firefox 中还需要一个标志来启用,并且在 Safari 中不支持。

坦率地说,除非你绝对确定确实需要它们,否则你可能不应该使用 containcontent-visibility,而且更有可能是通过 JavaScript 来设置和禁用它们。但当你确实需要它们时,它们就在那里。

浮动形状

让我们花点时间回到浮动元素的世界,并看看如何塑造文本流过它们的方式。老式网页设计师可能会记得技术,如 ragged floatssandbagging ——在这两种情况下,使用一系列宽度不同的短浮动图像来创建不整齐的浮动形状。由于 CSS Shapes 的出现,这些技巧不再需要了。

注意

未来,形状可能会用于非浮动元素,例如使用 CSS Grid 放置的元素,但截至 2022 年底,它们仅适用于浮动元素。

要围绕浮动元素来塑造内容流动,您需要定义一个形状。属性shape-outside就是您这样做的方式。

使用none,除了浮动元素本身的边距框外,没有任何形状——就像以前一样。这很直接也很无聊。是时候来点好东西了。

让我们从使用图像来定义浮动形状开始,因为它既简单又(在许多方面上)最令人兴奋。假设我们有一张新月形状的图像,我们希望内容围绕其可见部分流动。如果该图像具有透明部分,如 GIF 或 PNG 中所示,那么内容将流入这些透明部分,如图 20-17 所示:

img.lunar {float: left; shape-outside: url(moon.png);}
<img class="lunar" src="moon.png" alt="a crescent moon">

在大多数情况下,当您有一个浮动图像时,您通常会使用同一图像作为其形状。您并非一定要这样做——您可以始终加载第二张不同的图像来创建不匹配可见图像的浮动形状——但将单个图像同时用作浮动和其形状是迄今为止最常见的用例。在接下来的章节中,我们将讨论如何将内容推开离可见图像的部分,以及如何变化透明度阈值以确定形状;但现在,让我们尽情享受这种功能带来的力量。

image

图 20-17。使用图像定义浮动形状

在此阶段需要澄清一点:内容将流入其“直接访问”的透明部分,以便缺乏更好的术语。也就是说,内容不会同时流入图像的左右两侧(如图 20-17 所示),而只会流入右侧。这是因为右侧面向内容,这是左浮动图像。如果我们将图像右浮动,内容将流入图像左侧的透明区域。这在图 20-18(文本右对齐以使效果更明显)中有所说明:

p {text-align: right;}
img.lunar {float: right; shape-outside: url(moon.png);}

image

图 20-18。右侧的图像浮动形状

请始终记住,图像必须具有实际的透明区域才能创建形状。使用像 JPEG 这样的图像格式,或者即使您有一个没有 Alpha 通道的 GIF 或 PNG,形状将是一个矩形,就像您使用了shape-outside: none一样。

使用图像透明度进行形状塑造

正如您在前一节中看到的,可以使用具有透明区域的图像来定义浮动形状。图像的任何非完全透明部分都会创建形状。无论如何,这是默认行为,但您可以使用shape-image-threshold进行修改。

此属性让您决定透明度的哪个级别确定内容可以流入的区域,或者反之,哪个不透明度级别定义了浮动形状。 因此,使用 shape-image-threshold: 0.5,图像中透明度超过 50% 的任何部分都可以允许内容流入,而图像中透明度低于 50% 的任何部分都是浮动形状的一部分。 这在 图 20-19 中有所说明。

image

20-19。 使用图像不透明度来定义 50% 不透明度级别的浮动形状

如果您将 shape-image-threshold 属性的值设置为 1.0(或只是 1),则图像的任何部分都不能成为形状的一部分,因此不会有形状,并且内容将流过整个浮动。

另一方面,值为 0.0(或只是 0)将使图像的任何非透明部分成为浮动形状,就好像此属性根本未设置一样。 此外,任何低于 0 的值都重置为 0.0,而任何高于 1 的值都重置为 1.0

使用插入形状

现在让我们回到 <basic-shape> 和 <shape-box> 值。 基本形状是以下类型之一:

  • inset()

  • circle()

  • ellipse()

  • polygon()

此外,<shape-box> 可以是以下类型之一:

  • margin-box

  • border-box

  • padding-box

  • content-box

这些形状框指示形状的最外层限制。 您可以单独使用它们,如 图 20-20 中所示,图像具有一些填充,在其中可以看到深色背景颜色,然后是厚边框,最后是一些(始终不可见的)边距。

image

20-20。 基本形状框

默认形状框是边距框,这是合理的,因为在未被形状化时,浮动框使用它。 您可以结合基本形状使用形状框; 例如,您可以声明 shape-outside: inset(10px) border-box。 每种基本形状的语法不同,因此我们会依次介绍它们。

如果您习惯于使用边框图像,则插入形状应该看起来很熟悉。 即使您不熟悉,语法也不太复杂。 您可以定义距离从形状框的每一侧向内移动的距离,使用一个到四个长度或百分比值,并且可以选择一个角圆角值。

要选择一个简单的案例,假设我们想在形状框内部缩小形状 2.5 em

shape-outside: inset(2.5em);

创建四个偏移量,每个偏移量从形状框的外边缘向内 2.5 em。 在这种情况下,形状框是边距框,因为我们没有改变它。 如果我们希望形状从填充框收缩,值将像这样改变:

shape-outside: inset(2.5em) padding-box;

20-21 说明了我们刚刚定义的两个插入形状。

image

20-21。 从两个基本形状框中插入

与边距、填充、边框等一样,值复制也适用:如果少于四个长度或百分比,缺失的值将从给定值中派生。它们按照 TRBL 的顺序排列,因此以下对是内部等效的:

shape-outside: inset(23%);
shape-outside: inset(23% 23% 23% 23%);  /* same as previous */

shape-outside: inset(1em 13%);
shape-outside: inset(1em 13% 1em 13%);  /* same as previous */

shape-outside: inset(10px 0.5em 15px);
shape-outside: inset(10px 0.5em 15px 0.5em);  /* same as previous */

内嵌形状的一个有趣方面是在计算内嵌后能够圆角形状的能力。语法(和效果)与border-radius属性相同。因此,如果你想要给浮动形状加上 5 像素的圆角,你可以这样写:

shape-outside: inset(7%) round 5px;

另一方面,如果你想让每个角都呈椭圆形,使椭圆曲线高度为 5 像素,宽度为半个 em,你可以这样写:

shape-outside: inset(7% round 0.5em/5px);

在每个角设置不同的圆角半径也是可能的,并遵循通常的复制模式,只是从左上角开始而不是从顶部开始。因此,如果有多个值,它们的顺序是从左上角开始,顶部右侧,底部右侧,底部左侧(TL-TR-BR-BL,或 TiLTeR-BuRBLe),并通过复制声明的值填充缺失的值。图 20-22 展示了一些示例。(中间的圆角形状是浮动形状,为了清晰起见添加。浏览器实际上不会在页面上绘制浮动形状。)

image

图 20-22。圆角形状的角盒
注意

如果为浮动元素设置了border-radius值,这与创建具有圆角的平面形状并不相同。请记住,shape-outside默认为none,因此浮动元素的框不会受到边框的影响。如果你想让文本紧密地流过你用border-radius定义的边框圆角,你需要为shape-outside提供相同的圆角值。

圆形和椭圆形

圆形和椭圆形浮动形状使用类似的语法。在任何情况下,你都要定义形状的半径(或椭圆的两个半径),然后定义其中心的位置。

注意

如果你熟悉圆形和椭圆形渐变图像,定义圆形和椭圆形浮动形状的语法看起来非常相似。然而,本节将探讨一些重要的注意事项。

假设我们想要创建一个在其浮动中心的圆形形状,半径为 25 像素。我们可以通过以下任何一种方式实现:

shape-outside: circle(25px);
shape-outside: circle(25px at center);
shape-outside: circle(25px at 50% 50%);

无论我们使用哪种,结果都将如图 20-23 所示。

image

图 20-23。一个圆形浮动形状

需要注意的是,形状不能超出其形状框,即使您设置了一个看似可能的条件。例如,假设我们将前述的 25 像素半径规则应用于一个小图像,边长不超过 30 像素。在这种情况下,您将得到一个直径为 50 像素的圆,其圆心位于小于圆的矩形内部的矩形中。结果会怎样?圆可能被定义为突出到形状框的边缘之外——默认情况下是边界框——但它将在形状框的边缘处被剪切。因此,根据以下规则,内容将像没有形状一样流过图像,如图 20-24 所示:

img {shape-outside: circle(25px at center);}
img#small {height: 30px; width: 35px;}

image

图 20-24。一个非常小的圆形浮动形状,适用于更小的图像

我们可以看到圆形延伸超出图像的边缘,但请注意文本是如何沿着图像的边缘而不是浮动形状而流动的。这是因为实际的浮动形状被形状框剪切;在图 20-24 中,这是外边缘框,位于图像的外边缘处。因此,实际的浮动形状不是一个圆形,而是一个与图像尺寸完全相同的框。

无论您将形状框定义为何种边缘,都是如此。如果您声明shape-outside: circle(5em) content-box;,则形状将被剪切到内容框的边缘。内容将能够流过填充、边框和边距,并且不会以圆形方式推开。

这意味着您可以做一些事情,比如创建一个浮动形状,它是浮动框左上角圆的右下象限,假设图像是3em的正方形:

shape-outside: circle(3em at top left);

另外,如果您有一个完全正方形的浮动框,您可以定义一个圆形象限,该圆形象限刚好接触相反的边缘,使用百分比半径:

shape-outside: circle(50% at top left);

但请注意:这仅适用于浮动框是正方形的情况。如果是长方形,则会出现奇怪的情况。例如,以下示例将进行说明,该示例在图 20-25 中有插图:

img {shape-outside: circle(50% at center);}
img#tall {height: 150px; width: 70px;}

image

图 20-25。由矩形形成的圆形浮动形状

不要费心试图选择哪个维度控制了50%的计算,因为没有一个是。或者从某种意义上说,两者都是。

当您为圆形浮动形状的半径定义百分比时,它是相对于计算出的参考框来计算的。该框的高度和宽度如下计算:

css5 2026

实际上,这创建了一个正方形,它是浮动的固有高度和宽度的混合。对于我们的浮动图像,宽度为 70 像素,高度为 150 像素,这会导致一个边长为 117.047 像素的正方形。因此,圆的半径是这个值的 50%,即 58.5235 像素。

再次注意,图 20-26 中的内容流经图像并忽略了圆形。这是因为实际的浮动形状被形状框裁剪,所以最终的浮动形状将是一个类似垂直条形的形状,两端带有圆形,与 图 20-26 所示的非常相似。

图片

图 20-26. 裁剪后的浮动形状

把圆的中心定位并使其增长,直到触及到最接近圆心的一侧或最远离圆心的一侧,都是可行的技术,如此处所示,并在 图 20-27 中有所说明:

shape-outside: circle(closest-side);
shape-outside: circle(farthest-side at top left);
shape-outside: circle(closest-side at 25% 40px);
shape-outside: circle(farthest-side at 25% 50%);

图片

图 20-27. 各种圆形浮动形状

在 图 20-27 的一个示例中,形状被裁剪到其形状框,而在其他示例中,形状允许超出其框。如果我们没有裁剪形状,它将对图像过大!你将在下一个图中再次看到这一点。

现在,说到椭圆?除了使用名称 ellipse(),圆形和椭圆之间的唯一语法区别是你需要定义两个半径而不是一个。第一个是 x(水平)半径,第二个是 y(垂直)半径。因此,对于 x 半径为 20 像素和 y 半径为 30 像素的椭圆,你应该声明 ellipse(20px 30px)

在椭圆中,你可以使用任意长度或百分比,或者关键词 closest-sidefarthest-side 来定义椭圆的两个半径。图 20-28 展示了一些可能性。

图片

图 20-28. 使用椭圆定义浮动形状

与圆形不同,使用百分比来定义椭圆半径的长度稍有不同。椭圆中的百分比是相对于半径轴进行计算的。因此,水平百分比是相对于形状框的宽度计算的,垂直百分比是相对于高度计算的。这在 图 20-29 中有所说明。

图片

图 20-29. 椭圆形浮动形状和百分比

和任何基本形状一样,椭圆形状在形状框的边缘处被裁剪。

多边形

多边形的编写要复杂得多,尽管可能会稍微容易理解一些。你通过指定以逗号分隔的 x-y 坐标列表来定义多边形形状,可以是从形状框的左上角开始计算的长度或百分比,就像 SVG 中一样。每个 x-y 对都是多边形中的一个 顶点。如果第一个和最后一个顶点不同,浏览器会通过连接它们来闭合多边形。(所有多边形浮动形状必须是闭合的。)

所以假设我们想要一个 50 像素高和宽的菱形。如果我们从最顶端顶点开始构建多边形,polygon()值将如下所示:

polygon(25px 0, 50px 25px, 25px 50px, 0 25px)

百分比在background-image定位中的行为与此相同,因此我们可以定义一个菱形形状,始终“填满”形状框。它应该这样写:

polygon(50% 0, 100% 50%, 50% 100%, 0 50%)

这个和之前的多边形示例的结果显示在图 20-30 中。

图片

图 20-30。一个多边形浮动形状

这些例子都从最顶端顶点开始,但不必如此。以下所有方式都会得到相同结果:

polygon(50% 0, 100% 50%, 50% 100%, 0 50%) /* clockwise from top */
polygon(0 50%, 50% 0, 100% 50%, 50% 100%) /* clockwise from left */
polygon(50% 100%, 0 50%, 50% 0, 100% 50%) /* clockwise from bottom */
polygon(0 50%, 50% 100%, 100% 50%, 50% 0) /* counterclockwise from left */

与之前一样,请记住:如果一个形状定义超出了形状框,它将始终被剪切到形状框内。因此,即使你创建一个带有超出形状框(默认为边距框)的坐标的多边形,该多边形也将被剪切。图 20-31 演示了结果。

图片

图 20-31。当浮动形状超出形状框时如何剪切

多边形还有一个额外的复杂点:你可以切换它们的填充规则。默认情况下,填充规则是nonzero,但另一个可能的值是evenodd。展示它们之间区别比描述更容易,所以这里有一个星形多边形,有两种填充规则,详见图 20-32:

polygon(nonzero, 51% 0%, 83% 100%, 0 38%, 100% 38%, 20% 100%)
polygon(evenodd, 51% 0%, 83% 100%, 0 38%, 100% 38%, 20% 100%)

图片

图 20-32。两个多边形填充

默认的nonzero情况是我们填充多边形时倾向于考虑的:一个完全填充的单一形状。evenodd选项有不同的效果,其中一些多边形片段填充,而其他部分不填充。

这个特定的例子没有显示出太大差异,因为多边形的一部分缺失完全被填充的部分包围,所以无论哪种方式最终结果都是一样的。然而,想象一个有侧面尖刺的形状,然后一条垂直穿过它们中间的线。你不会得到一个梳子形状,而是一组不连续的三角形。有很多可能性。

正如你所想象的,一个多边形可能变得非常复杂,顶点数量很多。你可以在纸上计算每个顶点的坐标并输入,但使用工具会更加合理。这类工具的一个很好的例子是 Chrome Web Store 提供的 CSS Shapes Editor 扩展。 (Firefox 在其网络检查器中内置了此功能。)你可以在 DOM 检查器中选择一个浮动元素,打开 CSS Shapes Editor,选择一个多边形,然后在浏览器中创建和移动顶点,同时实时重新排列内容。一旦满意,你可以拖动选择并复制多边形值,以便粘贴到你的样式表中。图 20-33 展示了 Shapes Editor 在操作中的截图。

图片

图 20-33。Chrome Shapes Editor 的操作
警告

由于跨源资源共享(CORS)限制,除非它们通过 HTTP(S)从与 HTML 和 CSS 相同源服务器加载,否则不能使用形状编辑器编辑形状。从您的计算机加载本地文件将阻止形状可编辑。同样的限制阻止通过url()机制从本地存储加载形状。

添加形状边距

一旦定义了任何类型形状的浮动,就可以通过使用属性shape-margin向该形状添加“边距”——更正确地说是形状修改器

就像常规元素边距一样,形状边距通过长度或百分比将内容推开;百分比相对于元素包含块的宽度计算,就像常规边距一样。

形状边距的优点在于可以定义与要形状化的对象完全匹配的形状,然后使用形状边距创建额外空间。以基于图像的形状为例,其中图像的一部分可见,其余部分为透明。与其必须向图像添加不透明部分以使文本和其他内容远离图像的可见部分不同,可以直接添加形状边距。这样可以通过提供的距离扩大形状。

具体来说,通过从基本形状沿每个点垂直画一条长度等于shape-margin值的线来找到新形状中的点。在尖锐的角上,以该点为中心画一个半径等于shape-margin值的圆。之后,新形状是可以描述所有这些点和圆(如果有的话)的最小形状。

但请记住,形状永远不能超出形状框。因此,默认情况下,形状不能比未形状化的浮动的边距框更大。由于shape-margin实际上增加了形状的大小,任何超出形状框的新扩展形状部分将被裁剪。

要了解这意味着什么,考虑以下内容,如 图 20-34 所示:

img {float: left; margin: 0; shape-outside: url(star.svg);
    border: 1px solid hsl(0 100% 50% / 0.25);}
#one {shape-margin: 0;}
#two {shape-margin: 1.5em;}
#thr (shape-margin: 10%;}

image

图 20-34. 给浮动形状添加边距

注意内容在第二和第三个示例中的流动方式。有些地方内容确实比指定的shape-margin更接近,因为形状已在浮动元素的边距框内被裁剪。为了确保分离距离始终被观察到,包含标准边距,其距离等于或超过shape-margin距离。例如,我们可以通过修改两条规则来避免这个问题:

#two {shape-margin: 1.5em; margin: 0 1.5em 1.5em 0;}
#thr (shape-margin: 10%; margin: 0 10% 10% 0;}

在这两种情况下,右边和底边的边距被设置为与shape-margin值相同,确保扩展的形状在这些边上永远不会超过形状框。这在 图 20-35 中有所展示。

image

图 20-35. 确保形状边距不被裁剪

如果浮动元素向右移动,您将需要调整其边距以在下方和左侧创建空间,而不是右侧,但其原则是相同的。您还可以使用float: inline-endmargin-inline属性,以确保如果写入方向发生更改,布局仍按预期工作。

裁剪和遮罩

类似于浮动形状,CSS 还提供了元素的裁剪和遮罩功能,尽管没有任何元素框的形状。这些方法可以仅显示元素的部分内容,使用各种简单形状以及应用完整图像和 SVG 元素。这些方法可以使布局中的装饰性部分更加视觉上有趣,其中一种常见技术是为图像添加边框或不规则边缘。

裁剪

如果你只想在视觉上裁剪元素的一部分,可以使用clip-path属性。

使用clip-path,您可以定义裁剪形状。这基本上是元素内部可见部分绘制的区域。任何落在形状外部的部分将被裁剪掉,留下空的透明空间。下面的代码显示了同一段落的未裁剪和裁剪示例,结果显示在图 20-36 中:

p {background: orange; color: black; padding: 0.75em;}
p.clipped {clip-path: url(shapes.svg#cloud02);}

css5 2037

图 20-36。未裁剪和裁剪的段落

默认值none表示不执行裁剪,这可能是您所预期的。类似地,如果给出了<url>值(如前面的代码所示),并且它指向丢失的资源或 SVG 文件中不是<clipPath>的元素,则不执行裁剪。

警告

截至 2022 年底,基于 URL 的裁剪路径仅在大多数浏览器中工作,前提是 URL 指向与裁剪元素相同文档中的嵌入式 SVG。不支持外部 SVG。Firefox 是唯一支持来自外部 SVG 的裁剪路径的浏览器。

其余的值要么是用 CSS 编写的形状,要么是参考框,或者两者都有。

裁剪形状

您可以使用四个简单形状函数之一定义裁剪形状。这些与用于定义浮动形状的形状函数相同,因此我们在此不会详细描述它们。这里是一个简要回顾:

inset()

接受从一个到四个长度或百分比值,定义与边界框边缘的偏移量,并通过round关键字和另一组从一个到四个长度或百分比值可选地圆角化角。

circle()

接受一个长度、百分比或关键字,定义圆的半径,还可以用at关键字后跟一个或两个长度或百分比来定义圆的中心位置。

ellipse()

接受两个必填的长度、百分比或关键字,定义椭圆垂直和水平轴的半径,还可以用at关键字后跟一个或两个长度或百分比来定义椭圆的中心位置。

polygon()

接受逗号分隔的空格分隔的xy坐标列表,使用长度或百分比。可以以定义多边形填充规则的关键字为前缀。

图 20-37 展示了这些剪切形状的多种示例,对应以下样式:

.ex01 {clip-path: none;}
.ex02 {clip-path: inset(10px 0 25% 2em);}
.ex03 {clip-path: circle(100px at 50% 50%);}
.ex04 {clip-path: ellipse(100px 50px at 75% 25%);}
.ex05 {clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);}
.ex06 {clip-path: polygon(0 0, 50px 100px, 150px 5px, 200px 200px, 0 100%);}

css5 2038

图 20-37. 各种剪切形状

如图 20-37 所示,元素只在剪切形状内可见。超出其范围的部分则消失了。但请注意,被剪切的元素仍然占据了它们本来要占据的空间。换句话说,剪切并不会使元素变小,它只是限制了实际绘制的部分。

剪切框

不同于剪切形状,剪切框并不使用长度或百分比来指定。它们在很大程度上直接对应于盒模型中的边界。

如果只写clip-path: border-box,例如,元素会被剪切到边框的外边缘。这很可能是您所期望的,因为边距是透明的。然而,请记住,轮廓可以在边框外绘制,因此如果在边框边缘剪切,任何轮廓都将被剪切掉。这包括任何轮廓,这可能会导致重大的可访问性问题,因此在可以接收焦点的任何元素上进行剪切时要非常小心。(在这些情况下,您可能根本不应该这样做。)

当单独使用margin-boxpadding-boxcontent-box值时,剪切发生在外边距、内边距或内容区域的外边缘。这些在图 20-38 中有示意图。

css5 2039

图 20-38. 各种剪切框

还有图 20-38 的另一部分,展示了 SVG 边界框:

view-box

最近的(最接近的祖先)SVG 视口被用作剪切框。

fill-box

对象边界框用作剪切框。对象边界框是适合元素几何的最小框,考虑到任何变换(例如旋转),但不包括沿其外部的任何描边。

stroke-box

描边边界框用作剪切框。与填充框类似,描边框是适合元素几何的最小框,考虑到任何变换(例如旋转),但描边框包括沿其外部的任何描边。

这些值仅适用于没有关联 CSS 布局框的 SVG 元素。对于这些元素,如果给定了 CSS 样式框(margin-boxborder-boxpadding-boxcontent-box),则使用fill-box。相反,如果应用了 SVG 边界框值之一于CSS 布局框的元素(这是大多数元素),则使用border-box

有时使用类似 clip-path: content-box 这样的东西来裁剪掉内容区域外的所有内容可能很有用,但是这些框值在与裁剪形状结合使用时确实发挥了自己的作用。假设你有一个 ellipse() 裁剪形状要应用于一个元素,并且此外,你希望它刚好触及内边距框的外边缘。而不是必须通过减去边距和边框来计算必要的半径,你可以直接写 clip-path: ellipse(50% 50%) padding-box。这将在元素的中心处居中一个椭圆形裁剪形状,水平和垂直半径为元素参考框的一半,如 Figure 20-39 中所示,同时适配其他框的效果。

css5 2040

图 20-39. 将椭圆形裁剪形状适配到各种框中

注意到椭圆在 margin-box 示例中被截断了吗?那是因为边距是不可见的,所以虽然部分内容落在椭圆形裁剪区域内,但实际上我们看不到这些部分,除非元素上有阴影或外边框图片。

有趣的是,边界框关键字只能与裁剪形状一起使用,不能与基于 SVG 的裁剪路径一起使用。与 SVG 边界框相关的关键字仅在通过 CSS 裁剪 SVG 图像时应用。

使用 SVG 路径进行裁剪

如果你恰好有一个 SVG 路径方便使用,或者你对自己编写路径感到舒适,你可以将其用于定义 clip-path 属性中的裁剪形状。语法如下:

clip-path: path("…");

用 SVG 的 dpoints 属性替换那个省略号,这将给你一个裁剪形状。以下是这种属性的示例:

<path d="M 500,0 L 1000,250 L 500,500 L 0,250"/>

这将从点 x=500,y=0 开始绘制一个菱形,直到 x=1000,y=250,等等,形成一个宽度为 1,000 像素,高度为 500 像素的菱形。如果应用于精确为 1,000 像素乘以 500 像素的图像,你将得到 Figure 20-40 中显示的结果。

css5 2041

图 20-40. 使用 SVG 裁剪路径裁剪的图像

使用以下内容可以得到与 Figure 20-40 中显示的相同的裁剪形状:

clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0% 50%);

这里的区别在于,在多边形中使用百分比值定义的裁剪路径要比要求图像精确为 1,000 像素宽和 500 像素高的路径坐标更为强大。这是因为截至 2022 年末,所有 SVG 路径坐标都以绝对单位表示,不能像 polygon() 形状那样声明为图像高度和宽度的百分比。

注意

这只是对在 CSS 中使用 SVG 路径能力的简要介绍,因为描述路径形状的所有方式远远超出了本书的范围。如果你想了解更多,请阅读 Using SVG with CSS3 & HTML5 一书,作者是 Amelia Bellamy-Royds 等人(O’Reilly)。

蒙版

当我们说 遮罩 时,至少在这个上下文中,我们指的是一个形状,内部的东西是可见的,而外部是不可见的。因此,遮罩在概念上与裁剪路径非常相似。主要的区别有两点:首先,使用遮罩时,您只能使用图像来定义显示或裁剪的元素区域;其次,有更多的属性可用于遮罩,允许您执行诸如位置、大小和重复遮罩图像等操作。

警告

截至 2022 年底,Chromium 家族支持大多数遮罩属性,但仅支持 -webkit- 前缀。因此,例如,Chrome 和 Edge 支持 -webkit-mask-image 而非 mask-image

定义遮罩

应用遮罩的第一步是指向用于定义遮罩的图像。这通过 mask-image 实现,可以接受任何类型的图像。

假设图像引用有效,mask-image 将为浏览器提供要用作遮罩的图像。

我们将从一个简单的情况开始:将一个图像应用于另一个图像,两者高度和宽度相同。Figure 20-41 显示了两张单独的图像,同时第一张图像被第二张图像遮罩。

css5 2042

图 20-41. 一个简单的图像遮罩

如图所示,第二张图像中不透明部分显示第一张图像,透明部分则不显示。半透明部分则同时显示第一张图像的半透明效果。

下面是生成 Figure 20-41 所示结果的基本代码:

img.masked {mask-image: url(theatre-masks.svg);}

CSS 并不要求您仅将遮罩图像应用于其他图像。您可以将几乎任何元素与图像一起使用作为遮罩,该图像可以是光栅图像(GIF、JPG、PNG)或矢量图像(SVG)。如果有选择,后者通常是更好的选择。您甚至可以使用渐变构建自己的图像,无论是线性还是径向,重复或其他方式。

下列样式将显示为 Figure 20-42 所示:

*.masked.theatre {mask-image: url(i/theatre-masks.svg);}
*.masked.compass {mask-image: url(i/Compass_masked.png);}
*.masked.lg-fade {mask-image:
	repeating-linear-gradient(135deg, #000 0 1em, transparent 3em 4em);
}

css5 2043

图 20-42. 多样的图像遮罩

一个重要的要点是,当遮罩剪裁元素的部分时,它会将 所有 部分都裁剪掉。最好的例子是,当您应用一个遮罩图像来剪裁元素的外边缘时,列表项上的标记很容易变得不可见。Figure 20-43 显示了一个示例,这是以下操作的结果:

*.masked {mask-image: url(i/Compass_masked.png);}
<ol class="masked">
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
    <li>Four</li>
    <li>Five</li>
</ol>

css5 2044

图 20-43. 一个 PNG 图像,其中透明区域遮罩了一个无序列表

另一个选项允许您直接指向 SVG 中的 <mask> 元素来使用它定义的遮罩。这类似于从属性 clip-path 指向 <clipPath> 或其他 SVG 元素。以下是遮罩的定义示例:

<svg>
	<mask id="hexlike">
	   <path fill="#FFFFFF"
             d="M 50,0 L 100,25 L 100,75 L 50,100 L 0,75 L 0,25" />
	</mask>
</svg>

将 SVG 直接嵌入到 HTML 文件中时,可以像这样引用该遮罩:

.masked {mask-image: url(#hexlike);}

如果 SVG 在外部文件中,可以通过以下方式从 CSS 中引用它:

.masked {mask-image: url(masks.svg#hexlike);}

使用图像作为遮罩与使用 SVG <mask> 的不同之处在于,SVG 遮罩是基于亮度而不是 alpha 透明度的。可以通过 mask-mode 属性来反转这种差异。

更改遮罩的模式

刚才已经看到两种将图像用作遮罩的方法。通过将带有 alpha 通道的图像应用于另一个元素来实现遮罩。还可以通过使用遮罩图像的每个部分的亮度来定义遮罩。使用 mask-mode 属性可以在这两种选项之间切换。

三个值中的两个值很直观:alpha 意味着应使用图像的 alpha 通道来计算遮罩,而 luminance 意味着应使用亮度级别。差异在于 图 20-44 中有所体现,该图是以下代码的结果:

img.theatre {mask-image: url(i/theatre-masks.svg);}
img.compass {mask-image: url(i/Compass_masked.png);}
img.lum {mask-mode: luminance;}
<img src="i/theatre-masks.svg" role="img" alt="theater mask">
<img class="theatre" src="i/mask.jpg" alt="mask">
<img class="theatre lum" src="i/mask.jpg" alt="mask">
<img src="i/Compass_masked.png" alt="mask">
<img class="compass" src="i/mask.jpg" alt="mask">
<img class="compass lum" src="i/mask.jpg" alt="mask">

当使用 luminance 计算遮罩时,亮度处理方式类似于使用 alpha 值进行遮罩。考虑 alpha 值遮罩的工作方式:任何不透明度为 0 的部分会隐藏被遮罩元素的相应部分。不透明度为 1 的部分(完全不透明)显示被遮罩元素的相应部分。

基于亮度的遮罩处理也是如此。亮度为 1 的遮罩部分显示被遮罩元素的相应部分。亮度为 0(完全黑色)的遮罩部分隐藏被遮罩元素的相应部分。但要注意,任何完全透明的遮罩部分被视为亮度为 0。这就是为什么剧场面具图像的阴影部分不显示任何遮罩图像的原因:其 alpha 值大于 0。

css5 2045

图 20-44。Alpha 和亮度遮罩模式

第三个(也是默认的)数值是 match-source,结合了 alphaluminance,根据实际的源图像来选择使用哪一个,如下所示:

  • 如果源是 <image> 类型,则使用 alpha。 <image> 可以是诸如 PNG 或可见 SVG、CSS 渐变或通过 element() 函数引用的页面部分的图像。

  • 如果源是 SVG <mask> 元素,则使用 luminance

调整和重复遮罩

到目前为止,几乎所有的示例都经过精心设计,以使每个遮罩的大小与其遮罩的元素大小相匹配。(这就是为什么我们一直将遮罩应用于图像。)在许多情况下,遮罩图像的大小可能与被遮罩元素的大小不同。CSS 有几种处理方式,从 mask-size 开始。

如果你曾经调整过背景图像的尺寸,那么你完全知道如何调整遮罩的尺寸,因为值的语法完全相同,行为也是如此。例如,考虑下面的样式,其效果如 图 20-45 所示:

p {mask-image: url(i/hexlike.svg);}
p:nth-child(1) {mask-size: 100% 100%;}
p:nth-child(2) {mask-size: 50% 100%;}
p:nth-child(3) {mask-size: 2em 3em;}
p:nth-child(4) {mask-size: cover;}
p:nth-child(5) {mask-size: contain;}
p:nth-child(6) {mask-size: 200% 50%;}

css5 2046

图 20-45。调整遮罩尺寸

如果你曾经调整过背景大小,这些应该马上就能让你熟悉起来。如果没有,详细了解“调整背景图片大小”以探索更多可能性。

同样,正如背景的模式可以通过background-repeat进行更改或抑制一样,遮罩图像也可以通过mask-repeat受到影响。

这里提供的值与background-repeat相同。图 20-46 展示了一些示例,基于以下样式:

p {mask-image: url(i/theatre-masks.svg);}
p:nth-child(1) {mask-repeat: no-repeat; mask-size: 10% auto;}
p:nth-child(2) {mask-repeat: repeat-x; mask-size: 10% auto;}
p:nth-child(3) {mask-repeat: repeat-y; mask-size: 10% auto;}
p:nth-child(4) {mask-repeat: repeat; mask-size: 30% auto;}
p:nth-child(5) {mask-repeat: repeat round; mask-size: 30% auto;}
p:nth-child(6) {mask-repeat: space no-repeat; mask-size: 21% auto;}

css5 2047

图 20-46. 重复遮罩

定位遮罩

鉴于遮罩图像的大小和重复与背景图像的大小和重复相似,您可能会认为定位原始遮罩图像的方法与background-position类似,以及原点框的定义与background-origin类似。而这一切都是正确的。

如果您曾经定位过背景图像,那么您就知道如何定位遮罩图像。以下是几个示例,详见图 20-47:

p {mask-image: url(i/Compass_masked.png);
	mask-repeat: no-repeat; mask-size: 67% auto;}
p:nth-child(1) {mask-position: center;}
p:nth-child(2) {mask-position: top right;}
p:nth-child(3) {mask-position: 33% 80%;}
p:nth-child(4) {mask-position: 5em 120%;}

css5 2048

图 20-47. 定位遮罩

默认情况下,遮罩图像的原点框是外边框边缘。如果您希望将其移动到更深的内部,或在 SVG 上下文中定义特定的原点框,则mask-origin的作用类似于背景的background-origin

想要了解完整内容,请参阅“更改定位框”,但快速示例请参见图 20-48。

css5 2049

图 20-48. 更改原点框

裁剪和合成遮罩

另一个属性与背景相似,那就是mask-clip,遮罩版本的background-clip

所有这些操作只是将整体遮罩剪裁到受遮罩元素的特定区域。换句话说,它限制了元素可见部分的显示区域。图 20-49 展示了以下样式的结果:

p {padding: 2em; border: 2em solid purple; margin: 2em;
	mask-image: url(i/Compass_masked.png);
	mask-repeat: no-repeat; mask-size: 125%;
	mask-position: center;}
p:nth-child(1) {mask-clip: border-box;}
p:nth-child(2) {mask-clip: padding-box;}
p:nth-child(3) {mask-clip: content-box;}

css5 2050

图 20-49. 裁剪遮罩

最后一个长手遮罩属性mask-composite非常有趣,因为它可以根本改变多个遮罩之间的相互作用方式。

警告

截至 2023 年初,mask-composite仅由 Firefox 支持,但所有浏览器(包括 Firefox)都支持前缀形式-webkit-mask-composite

如果您对合成操作不熟悉,需要看一张图解。请参阅图 20-50。

css5 2051

图 20-50. 合成操作

在操作中位于顶部的图像称为源图像,而底部的图像称为目标图像

对于四种操作中的三种操作,即 addintersectexclude,无论图像是源还是目标,其结果都相同。但对于 subtract,问题是:从哪个图像中减去哪个?答案是:从目标中减去源。

在组合多个蒙版时,源和目标之间的区分也变得重要。在这些情况下,组合顺序是从后到前,每个后续层都是源,其下面已经组合好的层是目标。

要了解原因,请考虑 图 20-51,它显示了三个重叠蒙版如何组合在一起,以及随着其顺序和组合操作的更改,结果如何变化。

图形被构造成将最底部的蒙版放在底部,最顶部的放在其他两个之上,并将结果蒙版放在最顶部。因此,在第一列中,三角形和圆形通过排除操作进行组合。然后,使用加法操作将结果形状与正方形组合。这导致了显示在第一列顶部的蒙版。

只需记住,在进行减法复合时,底部形状从位于其上方的形状中减去。因此,在第三列中,三角形和圆形的加法被从它们上方的正方形中减去。这通过 mask-composite: add, subtract 来实现。

css5 2052

图 20-51. 复合蒙版

将所有内容结合起来

所有前述的蒙版属性都汇集在简写属性 mask 中。

像所有其他蒙版属性一样,mask 接受一个逗号分隔的蒙版列表。每个蒙版中的值的顺序可以是任意的,除了蒙版尺寸,它始终跟随位置,并由斜杠(/)分隔。

因此,以下规则是等效的:

#example {
    mask-image: url(circle.svg), url(square.png), url(triangle.gif);
    mask-repeat: repeat-y, no-repeat;
    mask-position: top right, center, 25% 67%;
    mask-composite: subtract, add;
    mask-size: auto, 50% 33%, contain;
}
#example {
    mask:
      url(circle.svg) repeat-y top right / auto subtract,
      url(square.png) no-repeat center / 50% 33% add,
      url(triangle.gif) repeat-y 25% 67% / contain;
}

三角形和正方形被加在一起,然后将这个加性复合物的结果从圆形中减去。结果显示在 图 20-52 中,应用于一个正方形元素(左侧的青色形状)和一个宽于高的形状(右侧的金黄色形状)。

css5 2053

图 20-52. 两个蒙版

设置蒙版类型

当您使用 CSS 样式化 SVG 元素,并且希望设置 SVG <mask> 类型时,mask-type 就派上用场了。

此属性类似于 mask-mode,只是没有 match-source 的等效项。您只能选择 luminancealpha

有趣的是,如果为用于掩蔽元素的 <mask> 元素设置了 mask-type,并且为该掩蔽元素声明了 mask-mode,则 mask-mode 优先。例如,请考虑以下规则:

svg #mask {mask-type: alpha;}
img.masked {mask: url(#mask) no-repeat center/cover luminance;}

给定这些规则,遮罩图像将具有亮度合成的遮罩,而不是 alpha 合成。如果mask-mode值保持默认值match-source,那么将使用mask-type的值。

边框图像遮罩

定义剪切路径和元素遮罩的同一规范,CSS 遮罩也定义了用于以与边框图像属性相似的方式应用遮罩图像的属性。实际上,除了一个例外,边框图像和边框遮罩之间的属性是直接对应的,值也相同。请参考“图像边框”以了解这些属性的工作原理,但这里有一些快速回顾。

请记住,如果没有任何边框,这些属性将不会产生任何可见效果。要应用边框然后对其进行遮罩,您必须首先声明边框的样式,至少是这样。如果您打算使遮罩边框宽度为 10 像素,您需要类似以下内容:

border: 10px solid;

一旦建立了这一点,您就可以开始遮罩边框。

注意

截至 2022 年底,所有这些属性在 Chromium 和 WebKit 浏览器中都支持-webkit-mask-box-image-*,而不是规范中使用的名称。实际支持的名称在接下来的属性摘要框中有说明,但示例使用标准(无前缀)属性名称。另请注意:截至目前,Gecko(Firefox)系列不支持任何形式的边框遮罩。

mask-border-source属性指定要用作遮罩的图像。这可以是 URL、渐变或其他支持的<image>值类型。一旦设置了遮罩图像,您可以继续进行诸如将其切片成部分、为遮罩定义独特宽度等操作。

mask-border-slice属性建立了一组四个切片线,这些线覆盖在边框上,它们的位置决定了遮罩将如何被切片用于边框区域的八个部分:顶部、右侧、底部和左侧边缘,以及左上、右上、右下和左下角。该属性最多接受四个值,按顺序定义从顶部、右侧、底部和左侧边缘的偏移量。

注意

截至 2022 年底,mask-border-slice没有逻辑属性等效项。如果对此属性的提议添加logical关键字或类似内容被采纳并实施,那么将可以以书写流相对方式使用mask-border-slice

考虑以下内容,在图 20-53 中有示意图:

#one {mask-border-slice: 25%;}
#two {mask-border-slice: 10% 20%;}
#thr {mask-border-slice: 10 20 15 30;}

css5 2054

图 20-53。一些遮罩边框切片模式

你可能认为数值偏移需要给定长度单位来定义距离,但实际情况并非如此。数字值在用于遮罩的图像坐标系统中进行解释。对于像 PNG 这样的光栅图像,坐标系统将是图像的像素。在 SVG 图像中,则使用 SVG 文件定义的坐标系统。

使用可选的fill关键字会导致遮罩图像的中心部分应用到边框区域内的元素上。默认情况下,它不会被使用,允许元素的填充和内容完全可见。如果你通过添加fill来使用它,则遮罩图像在四个切片线内的部分将被拉伸到元素的内容和填充上,并应用到它们上。请参考下面的说明,图示在图 20-54 中。

p {mask-border-image: url(circles.png);}
p.one {mask-border-slice: 33%;}
p.two {mask-border-slice: 33% fill;}

css5 2055

图 20-54。应用遮罩填充
警告

截至 2022 年底,浏览器中支持前缀属性的错误导致元素的内容和填充完全隐藏,除非使用fill关键字。因此,为了使用边框遮罩并显示元素的内容,你需要完全填充遮罩图像的中心,并使用fill

此属性允许你为边框遮罩的四个边缘切片定义宽度(或单独的宽度)。如果切片实际大小与声明的大小不符,它们将被调整大小以适应。例如,遮罩图像可能会被切片然后按以下方式调整大小:

mask-border-slice: 33%; mask-border-width: 1em;

这使你能够以一种方式切割遮罩图像,然后根据上下文需要调整其大小,或者定义一个通用的遮罩图像大小,无论其出现的上下文如何。

使用mask-border-outset,你可以将遮罩推到边框区域外。只有在你已经用border-image-outset将边框图像推到边框区域外并希望也将遮罩应用于该边框图像,或者已经对元素应用了轮廓并希望也遮罩它时,这很有用。如果两者都不是真的,则边框外的遮罩区域将仅遮罩边距区域,而该区域已经是透明的,因此不会明显改变。

警告

截至 2022 年底,支持前缀属性的浏览器不仅将片段推向外部,还会根据给定的量扩展中心区域,从而放大中心片段覆盖的掩膜区域。此行为在撰写本文时并未调用或显然未得到规范支持,很可能是一个错误(除非此行为最终被 CSS 工作组决定以回溯方式纠正)。

到目前为止,我们唯一的边框掩码示例使用了一个完全适合其掩码元素的掩码图像。这种情况不太可能发生,因为元素可以被任意数量的因素调整大小。默认情况下,将每个切片拉伸以适应其边框区域的部分,但也可能存在其他选项。图 20-55 说明了这些选项(为清晰起见,中心区域已被移除)。

css5 2056

图 20-55. 各种掩码图像重复方式

如图 20-55 所示,mask-border-repeat可以接受一个或两个重复值。如果给出一个,它应用于边框区域的所有边。如果给出两个,则第一个应用于边框区域的水平边,第二个应用于垂直边。

边框掩码具有一种样式方面,即使用mask-border-mode属性设置的图像边框没有的。

mask-border-mode属性设置掩码模式是基于透明度还是亮度。有关差异的更多细节,请参阅本章前面讨论的mask-mode属性。

mask-border属性将所有先前的边框掩码属性合并为一个便捷的简写形式。

对象适配与定位

另一种掩码的变化仅适用于像图像这样的替换元素。使用object-fit,你可以改变替换元素填充其元素框的方式 — 或者甚至让它完全不填充该框。

如果你曾经使用过background-size,那么这些值可能看起来很熟悉。它们也做类似的事情,只是用于替换元素。

例如,假设一个 50 × 50 像素的图像。我们可以通过 CSS 像这样改变它的大小:

img {width: 250px; height: 150px;}

默认期望是这些样式声明将使 50 × 50 的图像拉伸为 250 × 150。如果object-fit是其默认值fill,那么确实如此。

更改object-fit的值,将导致其他行为的发生。下面的示例在图 20-56 中进行了说明:

img {width: 250px; height: 150px; background: silver; border: 3px solid;}
img:nth-of-type(1) {object-fit: none;}
img:nth-of-type(2) {object-fit: fill;}
img:nth-of-type(3) {object-fit: cover;}
img:nth-of-type(4) {object-fit: contain;}

css5 2057

图 20-56. 四种对象适配方式

在第一个例子中,none<img>元素绘制为 250 像素宽,150 像素高。但是图像本身绘制为 50 × 50 像素(其固有大小),因为它被指定适合元素框。第二个例子中,fill,是默认行为,如前所述。这是唯一可能扭曲图像的值,因为尺寸是元素的尺寸,而不是图像的固有大小。

在第三个例子中,cover,图像被缩放直到元素框没有任何部分“未覆盖” — 但图像本身保持其固有的宽高比。换句话说,图像保持为正方形。在这种情况下,<img>元素的最长轴是250px长,因此图像被放大为 250 × 250 像素。然后将该 250 × 250 图像放置在 250 × 150 的<img>元素中。

第四个实例 contain 类似,只是图像只大到足以触及 <img> 元素的两侧。这意味着图像为 150 × 150 像素,并放置到其 <img> 元素的 250 × 150 像素框中。

重申一下,在 图 20-56 中所看到的是四个 <img> 元素。这些图像周围没有包装器 <div><span> 或其他元素。边框和背景色是 <img> 元素的一部分。放置在 <img> 元素内的图像根据 object-fit 进行调整。然后,<img> 元素的元素框就像是对其中适合的图像的简单遮罩一样起作用。(然后您可以使用本章早些时候介绍的属性来对元素框进行遮罩和裁剪。)

object-fit 的第五个值,未在 图 20-56 中表示,是 scale-downscale-down 的意思是“做与 nonecontain 相同的事情,以尽可能更小的尺寸为准。” 这使得图像始终保持其固有尺寸,除非 <img> 元素变得太小,此时会像 contain 一样缩小。这在 图 20-57 中有所说明,其中每个 <img> 元素都标有其给定的 height 值;每种情况下的 width 都为 100px

css5 2058

图 20-57. 多种 scale-down 场景

因此,如果替换元素比其适合的元素框大或小,我们如何影响其在该框内的对齐?使用 object-position 就是答案。

此处的值语法与 mask-positionbackground-position 相同,允许您在其元素框内定位替换元素,如果未设置为 object-fit: fill 的话。因此,给定以下 CSS,我们可以得到 图 20-58 中所示的结果:

img {width: 200px; height: 100px; background: silver; border: 1px solid;
     object-fit: none;}
img:nth-of-type(2) {object-position: top left;}
img:nth-of-type(3) {object-position: 67% 100%;}
img:nth-of-type(4) {object-position: left 142%;}

css5 2059

图 20-58. 多种 object-position 值的示例

请注意,第一个示例中的值是 50% 50%,即使在 CSS 代码中没有这个值。这说明 object-position 的默认值是 50% 50%。接下来的两个示例展示了各种 object-position 值如何在 <img> 元素框内移动图像。

如最后一个示例所示,可以将一个未缩放的替换元素(如图像)移动,使其部分被其元素框裁剪。这类似于将背景图像或蒙版定位,使其在元素边界处被裁剪。

对于比元素框大或小的替换元素,例如 object-fit: cover,也可以定位,尽管结果可能与 object-fit: none 很不同。以下 CSS 将产生类似 图 20-59 中所示的结果:

img {width: 200px; height: 100px; background: silver; border: 1px solid;
     object-fit: cover;}
img:nth-of-type(2) {object-position: top left;}
img:nth-of-type(3) {object-position: 67% 100%;}
img:nth-of-type(4) {object-position: left 142%;}

css5 2060

图 20-59. 定位覆盖对象

如果这些结果中有任何让您困惑的地方,请查看“背景图像定位”以获取更多详细信息。

总结

利用 CSS 作者可用的所有效果,我们可以获得无限种类的结果,因此可以创造出无限多种元素的创意呈现方式。不论是通过滤镜改变元素的外观,改变元素与背景混合的方式,剪裁或遮罩元素的部分,还是改变图像填充元素框的方式,现在您的选择空前丰富。

第二十一章:CSS At-Rules

现在已经探讨了 20 章关于可以组合以创建 CSS 规则的属性、值和选择器。这些可以称为普通规则常规规则,它们非常强大,但有时需要更多。有时候需要一种方式来封装某些条件块中的特定样式,以便在特定页面宽度或仅当浏览器处理样式表时识别给定的 CSS 特性时应用样式。

这些几乎总是包含在at-rules中,因为它们以 at(@)符号开头。您在之前的章节中已经看到了一些类似的内容,如@font-face@counter-style,但还有更多与样式具体细节不那么紧密相关的内容。本章探讨了三个强大的 at-rules @media@container@supports

媒体查询

由于 HTML 和 CSS 中定义的称为媒体查询的机制,您可以将任何一组样式(包括整个样式表)限制为特定媒体,如屏幕或打印,并限制到特定的媒体条件集。这些机制允许您定义媒体类型和条件的组合,如显示大小或颜色深度,举两个例子。我们将首先介绍基本形式,然后探索更复杂的形式。

基本媒体查询

对于基于 HTML 的样式表,您可以通过media属性对媒体进行限制。这对<link><style>元素同样适用:

<link rel="stylesheet" media="print"
    href="article-print.css">
<style media="print">
    body {font-family: sans-serif;}
</style>

media属性可以接受单个媒体值或逗号分隔的值列表。因此,要链接仅在screenprint媒体中使用的样式表,您可以这样写:

<link rel="stylesheet" media="screen, print"
    href="visual.css">

在样式表本身中,您还可以对@import规则施加媒体限制:

@import url(visual.css) screen;
@import url(article-print.css) print;

请记住,如果您不向样式表添加媒体信息,它将在所有媒体中应用。因此,如果您希望一组样式仅在屏幕上应用,另一组仅在打印中应用,您需要向两个样式表都添加媒体信息。例如:

<link rel="stylesheet" media="screen"
    href="article-screen.css">
<link rel="stylesheet" media="print"
    href="article-print.css">

如果从此示例中的第一个<link>元素中删除media属性,则将在所有媒体中应用样式表article-screen.css中找到的规则。

CSS 还定义了@media块的语法。这允许您在同一样式表中为多个媒体定义样式。考虑这个基本例子:

<style>
body {background: white; color: black;}
@media screen {
    body {font-family: sans-serif;}
    h1 {margin-top: 1em;}
}
@media print {
    body {font-family: serif;}
    h1 {margin-top: 2em; border-bottom: 1px solid silver;}
}
</style>

在所有媒体中,第一个规则给<body>元素设置了白色背景和黑色前景。这是因为其样式表,即由style属性定义的样式表,没有media属性,因此默认为all

注意

在这些块中显示的缩进仅用于清晰性目的。您无需对@media块内的规则进行缩进,但如果这样做可以使您的 CSS 更易于阅读,则可以这样做。

@media 块可以是任何大小,包含任意数量的规则。当作者只能控制单个样式表时,比如在共享托管环境或 CMS 中限制用户编辑的情况下,@media 块可能是定义特定媒体样式的唯一方式。这也适用于使用 XML 语言但不包含 media 属性或其等效项的情况下使用 CSS 样式文档。

这是三种广泛认可的媒体类型:

all

在所有呈现媒体中使用。

print

用于打印给视觉用户的文档,以及显示文档打印预览时使用。

screen

在屏幕媒体(如桌面计算机显示器或手持设备)上呈现文档时使用。所有在这类系统上运行的网络浏览器都是屏幕媒体用户代理。

完全有可能随着时间的推移添加新的媒体类型,因此请记住这个有限列表可能并不总是如此有限。例如,很容易想象 augmented-reality 作为一种媒体类型,因为增强现实显示中的文本很可能需要更高的对比度以突出显示背景现实。

HTML4 定义了一系列 CSS 最初识别的媒体类型,但大多数已被弃用,应该避免使用。这些包括 aural, braille, embossed, handheld, projection, speech, ttytv。如果您有使用这些媒体类型的旧样式表,几乎肯定应将其转换为三种被识别的媒体类型之一,如果可能的话。

注意

截至 2022 年,仍有一些浏览器支持 projection,允许文档以幻灯片形式呈现。几个移动设备浏览器也支持 handheld 类型,但方式不一致。

在某些情况下,可以将媒体类型组合成逗号分隔的列表,尽管这样做的理由并不是非常充分,因为目前可用的媒体类型数量很少。例如,样式可以限制为仅在屏幕和打印媒体上使用以下方式:

<link rel="stylesheet" media="screen, print"
    href="article.css">
@import url(article.css) print, screen;

@media screen,print {
    /* styles go here */
}

复杂的媒体查询

在前一节中,您看到如何使用逗号将多个媒体类型链接在一起。我们可以称之为 复合媒体查询,因为它允许我们同时处理多种媒体。不过,媒体查询还有很多内容:不仅可以基于媒体类型应用样式,还可以基于这些媒体的特性,如显示大小或颜色深度。

这是一种非常强大的功能,单靠逗号并不足以实现所有这些。因此,CSS 包括逻辑运算符 and 来将媒体类型与这些媒体的特性配对。

让我们看看这在实践中如何运作。以下是在彩色打印机上渲染文档时应用外部样式表的两种基本等效方式:

<link href="print-color.css"
    media="print and (color)" rel="stylesheet">
@import url(print-color.css) print and (color);

在可以提供媒体类型的任何地方,都可以构建媒体查询。这意味着,在前一节的示例之后,可以以逗号分隔的列表形式列出多个查询:

<link href="print-color.css"
   media="print and (color), screen and (color)" rel="stylesheet">
@import url(print-color.css) print and (color), screen and (color);

如果多个媒体查询中的任何一个评估为true,则将应用关联的样式表。因此,根据先前的@import,如果渲染到彩色打印机彩色屏幕环境,将应用print-color.css。如果打印到黑白打印机,则两个查询都将评估为falseprint-color.css将不会应用于文档。在灰度屏幕环境、任何语音媒体环境等情况下也是如此。

每个媒体描述符由媒体类型和一个或多个列出的媒体特性组成,每个媒体特性描述符都用括号括起来。如果未提供媒体类型,则假定为all,这使得以下两个示例等效:

@media all and (min-resolution: 96dpi) {…}
@media (min-resolution: 96dpi) {…}

一般来说,媒体特性描述符的格式与 CSS 中的属性值对类似,只是被括号括起来。存在一些差异,最显著的是一些特性可以在没有伴随值的情况下指定。例如,任何基于颜色的介质将使用(color)进行匹配,而使用 16 位颜色深度的任何颜色介质将使用(color: 16)进行匹配。实际上,使用没有值的描述符是对该描述符进行真/假测试的方式:(color)表示“这个介质是彩色的吗?”

多个特性描述符可以使用and逻辑关键字链接。事实上,媒体查询中有两个逻辑关键字:

and

将两个或多个媒体特性连接在一起,要求所有这些特性都必须为真才能使查询为真。例如,(color) and (orientation: landscape) and (min-device-width: 800px)意味着必须同时满足这三个条件:如果媒体环境有颜色,处于横向方向,设备的显示器宽度至少为 800 像素,那么样式表将被应用。

not

对整个查询进行否定,如果所有条件都为真,则不应用样式表。例如,not (color) and (orientation: landscape) and (min-device-width: 800px)意味着如果三个条件都满足,则该语句被否定。因此,如果媒体环境有颜色,处于横向方向,设备的显示器宽度至少为 800 像素,则样式表会使用。在所有其他情况下,它将被使用。

CSS 没有or逻辑关键字,因为其作用由逗号代替,如前所示。

注意not关键字只能在媒体查询的开头使用。目前不允许写像(color) and not (min-device-width: 800px)这样的语句。在这种情况下,整个查询块将被忽略。

让我们考虑一个例子,了解所有这些是如何发挥作用的:

@media screen and (min-resolution: 72dpi) {
	.cl01 {font-style: italic;}
}
@media screen and (min-resolution: 32767dpi) {
	.cl02 {font-style: italic;}
}
@media not print {
	.cl03 {font-style: italic;}
}
@media not print and (monochrome) {
	.cl04 {font-style: italic;}
}

图 21-1 显示了结果,但请记住,即使您可能正在纸上阅读本文,实际图像是由屏幕介质浏览器生成的(例如 Firefox Nightly),显示了应用了前述 CSS 的 HTML 文档。因此,您在 图 21-1 中看到的所有内容都是在 screen 媒介下操作的。

css5 2101

图 21-1. 媒体查询中的逻辑运算符

第一行被斜体化,因为显示文件的屏幕分辨率达到或超过每英寸 72 点。然而,它的分辨率并不是 32767dpi 或更高,因此第二个媒体块被跳过,因此第二行保持未斜体化。第三行被斜体化,因为它是屏幕显示,并非打印。最后一行被斜体化,因为它既非打印也非单色——在这种情况下,是非单色。

另一个关键字 only 是为了创建有意的向后不兼容性。是的,真的。

only

用于隐藏样式表,适用于那些理解媒体查询但不理解媒体类型的过时浏览器。(在现代用法中,这几乎从不是问题,但这种能力是创造出来的,所以我们在这里记录它。)在理解媒体类型的浏览器中,only 关键字会被忽略并应用样式表。在不理解媒体类型的浏览器中,only 关键字会创建一个名为 only all 的表象媒体类型,这是无效的。

特殊值类型

通过媒体查询引入了两种值类型。这些类型与特定的媒体特性结合使用,后面将对其进行解释:

<ratio>

两个由斜杠 (/) 分隔的数字,在 第 5 章 中有定义。

<resolution>

分辨率值是正整数 <integer>,后跟单位标识符 dpidpcm。在 CSS 术语中,dot 是任何显示单元,其中最常见的是像素。通常情况下,<integer> 和标识符之间不允许空白。因此,具有恰好 150 像素(点)每英寸的显示器匹配 150dpi

关键词媒体特性

到目前为止,您在示例中看到了几个媒体特性,但没有完整的可能特性及其值列表。现在让我们来修正这一点!

请注意,以下值均不可为负,并且媒体特性始终用括号括起来:

媒体特性:any-hover

值:none | hover

检查是否存在任何可用于悬停在元素上的输入机制(即触发 :hover 状态)。none 值表示没有这样的机制,或者没有方便地执行此操作的机制。与 hover 媒体特性进行比较,后者限制检查到主要输入机制。

媒体特性:any-pointer

值:none | coarse | fine

检查创建屏幕指针的输入机制。none值表示没有这样的设备,coarse表示至少有一台精度有限的设备(例如手指),fine表示至少有一台精度较高的设备(例如鼠标)。与pointer相比,后者限制检查到主要输入机制。

媒体特性:color-gamut

值:srgb | p3 | rec2020

测试浏览器和输出设备支持的色彩范围。截至 2022 年底,大多数显示器支持srgbp3色域。p3值指的是 Display P3 色彩空间,它是 sRGB 的超集。rec2020值指的是 ITU-R 推荐 BT.2020 色彩空间,它是 P3 的超集。截至 2022 年底,Firefox 不支持color-gamut媒体特性。

媒体特性:display-mode

值:fullscreen | standalone | minimal-ui | browser

测试顶级浏览上下文及任何子浏览上下文的显示模式。这对应于 Web 应用程序清单规范的display成员,并常用于检查渐进式 Web 应用访客是否在浏览网站或已安装的应用程序上,但无论是否定义了清单,都适用。详细信息请参见“强制颜色、对比度和显示模式”。

媒体特性:dynamic-range

值:standard | high

检查浏览上下文是否支持视觉输出的高动态范围。high 值表示媒体环境支持高峰值亮度、高对比度比率和 24 位或更高的色彩深度。高峰值亮度或色彩对比度没有明确定义的值,因此由浏览器决定。任何符合 high 的设备也会符合 standarddynamic-range 媒体特性在 2022 年初获得了广泛的浏览器支持。

媒体特性:forced-colors

值:none | active

检查浏览器是否处于强制颜色模式,该模式强制使用浏览器默认值一组 CSS 属性,如colorbackground-color,以及少数其他属性的特定值,并可能触发prefers-color-scheme值。详细信息请参见“强制颜色、对比度和显示模式”。截至 2022 年底,WebKit 不支持forced-colors媒体特性。

媒体特性:grid

值:0 | 1

指涉基于网格的输出设备的存在(或不存在),例如 TTY 终端。这与 CSS 网格无关。基于网格的设备将返回1;否则,返回0。此媒体特性可替代旧的tty媒体描述符。

媒体特性:hover

值:none | hover

检查用户的主要输入机制是否能够悬停在元素上。none 表示主要机制无法悬停,或无法方便地悬停;例如,移动设备在执行不方便的点击并保持动作时会模拟悬停。hover 表示悬停是方便的,例如使用鼠标。与 any-hover 相比,后者检查任何机制是否允许悬停,而不仅仅是主要机制。

媒体特性:inverted-colors

值:none | inverted

检查操作系统是否反转颜色。none 表示颜色正常显示;inverted 表示显示区域内的所有像素都被反转。截至 2022 年末,仅 WebKit 支持 inverted-colors 媒体特性。

媒体特性:orientation

值:portrait | landscape

指用户代理显示区域的方向,如果媒体特性 height 大于或等于媒体特性 width,返回 portrait。否则,结果为 landscape

媒体特性:overflow-block

值:none | scroll | optional-paged | paged

检查输出设备如何处理沿块轴溢出的内容。none 表示无法访问溢出的内容;scroll 表示内容可以通过某种方式滚动访问;optional-paged 表示用户可以滚动内容,但可以使用像 break-inside 这样的属性手动触发分页;paged 表示溢出内容只能通过“翻页”来访问,例如电子书。截至 2022 年末,仅 Firefox 支持 overflow-block 媒体特性。

媒体特性:overflow-inline

值:none | scroll

检查输出设备如何处理沿内联轴溢出的内容。none 表示无法访问溢出的内容;scroll 表示内容可以通过滚动访问。截至 2022 年末,仅 Firefox 支持 overflow-inline 媒体特性。

媒体特性:pointer

值:none | coarse | fine

检查主要输入机制是否在屏幕上创建指针。none 表示主要输入设备不生成指针,coarse 表示生成指针但精度有限,fine 表示生成高精度指针(例如鼠标)。与 any-pointer 相比,后者检查任何机制是否生成指针,而不仅仅是主要机制。

媒体特性:prefers-color-scheme

值:light | dark

检查用户在浏览器或操作系统级别选择的颜色方案(例如,亮色模式或暗色模式)。因此,作者可以为 prefers-color-scheme: dark 定义特定的颜色值。Safari 添加了 no-preference 值,但截至 2022 年末尚未标准化或被其他浏览器采纳。

媒体特性:prefers-contrast

值:no-preference | less | more | custom

检查用户是否在浏览器或操作系统级别(例如 Windows 高对比度模式)设置了对高对比度输出的偏好。详见 “强制颜色、对比度和显示模式” 获取详细信息。

媒体特性:prefers-reduced-motion

值:no-preference | reduce

检查用户是否在浏览器或操作系统级别设置了关于动态的偏好。reduce 值表示用户表示希望减少或消除动态,可能是由于前庭障碍导致在屏幕上观看动态时感到不适。出于可访问性原因,转换和动画应经常放入 prefers-reduced-motion: reduce 块中。

媒体特性:scan

值:progressive | interlace

指的是输出设备中使用的扫描过程。interlace 值通常用于 CRT 和一些等离子显示器。截至 2022 年末,所有已知的实现都与 progressive 值匹配,使得此媒体特性有些无用。

媒体特性:scripting

值:none | initial-only | enabled

检查是否支持 JavaScript 等脚本语言。initial-only 值表示只能在页面加载时执行脚本,之后不能执行。截至 2022 年末,scripting 媒体特性尚未被任何浏览器支持。

媒体特性:update

值:none | slow | fast

检查页面加载后是否可以更改内容的外观。none 值表示不可能更新,例如在印刷媒体中。slow 值表示由于设备或浏览器约束,变更可能,但不能平稳动画。fast 值表示可以平滑动画。截至 2022 年末,update 媒体特性仅受 Firefox 支持。

媒体特性:video-dynamic-range

值:standard | high

检查浏览上下文是否支持视频的高动态范围视觉输出。这很有用,因为某些设备单独渲染视频与其他图形不同,因此可能支持与其他内容不同的视频动态范围。high 值表示媒体环境支持高峰值亮度、高对比度比和 24 位或更高的色彩深度。高峰值亮度或色彩对比度没有精确定义的值,因此由浏览器决定。与 high 匹配的任何设备也将匹配 standardvideo-dynamic-range 媒体特性在 2022 年初获得了广泛的浏览器支持。

强制颜色、对比度和显示模式

其中三个之前定义的媒体特性与用户显示偏好有关,并允许您检测这些偏好以便进行相应的样式设置。其中两个紧密相关,因此我们将从它们开始。

如果用户已经努力定义了用于显示其内容的特定颜色集合,例如 Windows 高对比度模式,则将匹配forced-colors: active,以及prefers-contrast: custom。您可以使用其中一个或两者来在这种情况下应用特定样式。

如果forced-colors: active返回 true,则以下 CSS 属性将被强制使用浏览器(或操作系统)的默认值,覆盖您可能声明的任何值:

  • background-color

  • border-color

  • color

  • column-rule-color

  • outline-color

  • text-decoration-color

  • text-emphasis-color

  • -webkit-tap-highlight-color

另外,SVG 的fillstroke属性将被忽略并设置为它们的默认值。

此外,以下属性-值组合将强制使用,而不管作者声明了什么:

  • box-shadow: none

  • text-shadow: none

  • background-image: none 对于非基于 URL 的值(例如渐变色)

  • color-scheme: light dark

  • scrollbar-color: auto

这意味着,举一个例子,任何依赖于改变边框颜色的悬停或焦点样式的元素将无法生效。因此,您可以改变字体粗细和边框样式(而不是颜色):

nav a[href] {border: 3px solid gray;}
nav a[href]:is(:hover, :focus) {border-color: red;}

@media (forced-colors: active) {
	:hover {font-weight: bold; border-style: dashed;}
}

这是一个为适应强制颜色情况而进行的小改变的示例,通过这些改变提供更大的可用性。您不应该使用此查询为强制某些颜色的用户设置一个完全不同的设计。

正如前面所述,如果用户设置了forced-colors: active,则会触发prefers-contrast: custom。这个媒体特性的值的含义如下:

no-preference

浏览器和/或操作系统不知道用户在颜色对比方面的偏好。

less

用户请求具有低于通常对比度的界面。例如,偏头痛或阅读障碍(并非所有的阅读障碍者都如此)的用户可能会发现高对比度的文本难以阅读。

more

用户请求具有比通常更高对比度的界面。

custom

用户已经定义了一组特定的颜色,这些颜色既不匹配more也不匹配less,例如 Windows 高对比度模式。

在这种情况下,可以通过不提供值来查询任何值,这在这种情况下尤为有用。您可以如下为低对比度和高对比度用户提供服务:

body {background: url(/assets/img/mosaic.png) repeat;}

@media (prefers-contrast) {
	body {background-image: none;}
}

display-mode 媒体特性与前两个特性完全不同。display-mode 媒体特性允许作者确定正在使用的显示环境,并相应地进行操作。

首先让我们定义各个值的含义:

fullscreen

应用程序占据整个可用的显示区域,不显示任何应用程序的界面元素(例如地址栏、返回按钮、状态栏等)。

standalone

应用程序看起来像一个本地独立应用程序。这会去除地址栏等应用程序界面,但会使操作系统衍生的导航元素如返回按钮可用。

minimal-ui

应用程序看起来类似于本地独立应用程序,但提供了一种访问应用程序界面的方式,例如地址栏,应用程序的导航控制等等。还可以包括用于“分享”或“打印”等系统特定界面控件。

浏览器

应用程序正常显示,显示完整的应用程序界面,包括完整的地址栏(带有前进/后退/主页按钮)、滚动条间隙等等。

这些各种状态可以由用户将浏览器放入特定模式(例如,用户在 Windows 上按下 F11 进入全屏模式)触发,或者由 Web 应用程序清单的display成员触发。在所有方面,这些值都完全相同;事实上,Web 应用程序清单规范只是指向 CSS 媒体查询第 5 级规范中定义的这些值。

因此,您可以定义不同显示模式的不同布局。以下是一个简短的示例:

body {display: grid; /* add column and row templates here */}

@media (display-mode: fullscreen) {
	body { /* different column and row templates here */}
}
@media (display-mode: standalone) {
	body { /* more different column and row templates here */}
}

如果您计划在多个上下文中使用设计,例如在网页浏览器中,作为 Web 应用程序,或在信息亭等地方,这将特别有用。

范围媒体特性

现在我们将注意力转向媒体特性,这些特性允许范围,并且还有min--max变体,除了接受像长度或比率这样的值之外。它们还有一种更紧凑的格式化值比较的方式,将在接下来的部分讨论。

媒体特性:widthmin-widthmax-width

值:<长度>

用户代理视口的宽度。在屏幕媒体的 Web 浏览器中,这是视口的宽度加上任何滚动条。在分页媒体中,这是页面框的宽度,即页面中呈现内容的区域。因此,(min-width: 100rem)适用于视口宽度大于或等于 100 rem 的情况。

媒体特性:heightmin-heightmax-height

值:<长度>

用户代理视口的高度。在屏幕媒体的 Web 浏览器中,这是视口的高度加上任何滚动条。在分页媒体中,这是页面框的高度。因此,(height: 60rem)适用于视口高度正好为 60 rems 的情况。

媒体特性:aspect-ratiomin-aspect-ratiomax-aspect-ratio

值:<比率>

通过比较width媒体特性和height媒体特性得出的比率(请参阅“特殊值类型”中的<比率>定义)。因此,(min-aspect-ratio: 2/1)适用于宽高比至少为 2:1 的任何视口。

媒体特性:colormin-colormax-color

值:<整数>

输出设备中颜色显示能力的存在,可选 数值表示每个颜色分量中使用的位数。因此,(color) 适用于任何具有任何颜色深度的设备,而 (最小颜色: 4) 意味着每个颜色分量必须至少使用 4 位。任何不支持颜色的设备将返回 0

媒体特性:颜色索引最小颜色索引最大颜色索引

值:<整数>

输出设备颜色查找表中可用颜色的总数。任何不使用颜色查找表的设备将返回 0。因此,(最小颜色索引: 256) 适用于任何至少有 256 种颜色可用的设备。

媒体特性:单色最小单色最大单色

值:<整数>

单色显示的存在,输出设备帧缓冲区中每个像素的 可选 位数。任何不是单色的设备将返回 0。因此,(单色) 适用于任何单色输出设备,而 (最小单色: 2) 意味着输出设备的帧缓冲区中每个像素至少有 2 位。

媒体特性:分辨率最小分辨率最大分辨率

值:<分辨率>

输出设备的像素密度,以每英寸点数(dpi)或每厘米点数(dpcm)测量;有关详细信息,请参阅下一节中 <分辨率> 的定义。如果输出设备具有非正方形的像素,则使用最低密度的轴;例如,如果一个设备在一个轴上是 100 dpcm,在另一个轴上是 120 dpcm,则返回100。此外,在这种非正方形情况下,一个裸的分辨率特性查询,即没有值的查询,永远不会匹配(尽管 最小分辨率最大分辨率 可以)。请注意,分辨率值必须不仅非负,而且非零。

对于带有范围的媒体特性值,通常希望通过最大值和最小值将规则限制在特定范围内。例如,您可能希望在两个显示宽度之间应用一定的边距,如下所示:

@media (min-width: 20em) and (max-width: 45em) {
	body {margin-inline: 0.75em;}
}

媒体查询级别 4 定义了一种更紧凑的方式来表达相同的内容,使用标准数学表达式如等于、大于、小于等。因此,前面的例子可以重写如下:

@media (20em < width < 45em) {
	body {margin-inline: 0.75em;}
}

换句话说,“宽度大于 20 em 并且小于 45 em”。如果要使媒体块中的规则恰好应用于 20 和 45 em 的宽度,则应将 < 符号写为 <=

此语法可以用来限制只有一个方向,就像这个例子所示:

@media (width < 64rem) {
	/* tiny-width styles go here */
}
@media (width > 192rem) {
	/* enormous-width styles go here */
}

任何接受值范围的媒体特性(请参阅上一节)都可以使用此语法格式。这实际上消除了在特性名称上需要 最小-最大- 前缀以及复杂 and 结构的需要。

您还可以通过使用and组合符号将多个范围查询串联起来,如下所示:

@media (20em < width < 45em) and (resolution =< 600dpi) {
	body {margin-inline: 0.75em;}
}

当显示区域的宽度在 20 到 45 em 之间,并且输出分辨率低于 600 dpi 时,这将向<body>元素添加内联边距。

警告

截至 2023 年初,Chrome 和 Firefox 浏览器系列支持紧凑范围语法,Safari 在其夜间版本中也有。我们希望这在本版出版后不久(甚至之前!)在所有地方都得到支持。

弃用的媒体特性

下列媒体特性已被弃用,因此浏览器可能随时停止支持它们。我们在这里包含它们是因为您可能在旧版 CSS 中遇到它们,并且需要知道它们的用途,以便将其替换为更现代的内容。

媒体特性:device-widthmin-device-widthmax-device-width

最佳替代为:widthmin-widthmax-width

值:<length>

输出设备的完整渲染区域的宽度。在屏幕媒体中,这是屏幕的宽度(即手持设备屏幕或桌面监视器的水平测量)。在分页媒体中,这是页面本身的宽度。因此,当设备的输出区域小于或等于 1200 像素宽时,(max-device-width: 1200px)生效。

媒体特性:device-heightmin-device-heightmax-device-height

最佳替代为:heightmin-heightmax-height

值:<length>

输出设备的完整渲染区域的高度。在屏幕媒体中,这是屏幕的高度(即手持设备屏幕或桌面监视器的垂直测量)。在分页媒体中,这是页面本身的高度。因此,当设备的输出区域小于或等于 400 像素高时,(max-device-height: 400px)生效。

媒体特性:device-aspect-ratiomin-device-aspect-ratiomax-device-aspect-ratio

最佳替代为:aspect-ratiomin-aspect-ratiomax-aspect-ratio

值:<ratio>

通过比较device-width媒体特性和device-height媒体特性的比率得出的比率(请参见“特殊值类型”中对ratio的定义)。因此,当输出设备的显示区域宽高比恰好为 16:9 时,(device-aspect-ratio: 16/9)生效。

响应式样式

媒体查询是“响应式网页设计”的基础。通过根据显示环境应用不同的规则集,可以将“手机友好”和“桌面友好”的样式融合到单个样式表中。

我们将这些术语放在引号中,因为您可能在自己的生活中已经看到移动设备和桌面设备之间的界限变得模糊。例如,具有可折叠触摸屏的笔记本电脑可以兼具平板电脑和笔记本电脑的功能。例如,CSS(尚未)无法检测到铰链是否超过某一点,也无法检测设备是手持还是放置在平面上。而是从媒体环境的各个方面进行推断,如显示大小或显示方向。

在响应式设计中,一个相当常见的模式是为每个@media块定义断点。通常采用某些像这样的像素宽度:

/* …common styles here… */
@media (max-width: 400px) {
    /* …small-screen styles here… */
}
@media (min-width: 401px) and (max-width: 1000px) {
    /* …medium-screen styles here… */
}
@media (min-width: 1001px) {
    /* …big-screen styles here… */
}

这使得某些设备可以显示的内容和其报告方式做出了某些假设。例如,iPhone 6 Plus 的分辨率为 1,242 × 2,208,它将其降采样到 1,080 × 1,920。即使在降采样分辨率下,这足够多的像素宽度来符合前面例子中的大屏幕样式。

但等等!iPhone 6 Plus 还保持了一个内部坐标系统,测量为 414 × 736。如果它决定将这些作为其像素定义,这完全是有效的,那么它将只会获得小屏幕样式。

这里的重点不是单独指出 iPhone 6 Plus 是唯一不好的设备,事实并非如此,而是说明依赖基于像素的媒体查询的不确定性。浏览器制造商已经尽力使他们的浏览器表现得有些让人信服,但从来没有完全如我们所愿,而且您永远不知道新设备的假设何时会与您的假设发生冲突。

还有其他方法可用,但它们带来了自己的不确定性。与其使用像素,您可能会尝试基于 em 的措施,类似于这样:

/* …common styles here… */
@media (max-width: 20em) {
    /* …small-screen styles here… */
}
@media (min-width: 20.01em) and (max-width: 50em) {
    /* …medium-screen styles here… */
}
@media (min-width: 50.01em) {
    /* …big-screen styles here… */
}

这将断点与文本显示大小而非像素绑定在一起,这更加健壮。不过,这也并非完美:它依赖于合理的方法来确定智能手机的 em 宽度。它还直接依赖于设备实际使用的字体系列和大小,这在不同设备之间有所不同。

这里是另一个看似简单的查询集合,但可能会带来意外的结果:

/* …common styles here… */
@media (orientation: landscape) {
    /* …wider-than-taller styles here… */
}
@media (orientation: portrait) {
    /* …taller-than-wider styles here… */
}

这感觉像是判断智能手机是否正在使用的好方法:毕竟,它们大多比宽要高,大多数人不会把它们横过来阅读。但是orientation特性指的是heightwidth;也就是说,如果height大于或等于width,则orientationportrait。而不是device-heightdevice-width,而是指用户代理的显示区域的heightwidth

这意味着,如果假设“纵向等同于智能手机”,那么一些桌面用户可能会感到惊讶,因为他们的显示区域(浏览器边框内的部分)比宽高更高,甚至是完全正方形。

基本观点在于响应式样式很强大,就像任何强大的工具一样,它的使用需要充分的思考和谨慎。仔细考虑每个特性查询组合的影响是成功响应的最低要求。

分页媒介

在 CSS 术语中,分页媒介指的是以一系列离散“页面”呈现文档演示的任何媒介。这与屏幕有所不同,屏幕是连续媒介:文档呈现为单个可滚动的“页面”。连续媒介的模拟例子是纸莎草卷。印刷物料,如书籍、杂志和激光打印品,都是分页媒介。幻灯片演示也是分页媒介,每张幻灯片在 CSS 术语中都是一个“页面”。

打印样式

即使在无纸化的未来,最常见的分页媒介仍然是文档的打印品—网页、文字处理文件、电子表格或其他的已经被提交到薄薄的死树片中。您可以做一些事情,让您的文档打印品对用户更加愉悦,例如调整页面断点或创建专门用于打印的样式。

请注意,打印样式也会应用到打印预览模式中的文档显示上。因此,在某些情况下,在监视器上会看到打印样式。

屏幕和印刷之间的差异

除了明显的物理差异外,屏幕设计和印刷设计之间也存在风格差异。最基本的差异在于字体选择。大多数设计师会告诉你,无衬线字体更适合屏幕设计,但印刷中使用衬线字体更易读。因此,您可能需要设置一个印刷样式表,将文档中的文本使用 Times 而不是 Verdana。

另一个主要的差异涉及字体大小。如果您花费了任何时间进行网页设计,您可能一遍又一遍(再一遍)听到点阵字在网页上是一个糟糕的选择。这基本上是正确的,特别是如果您希望您的文本在各种浏览器和操作系统之间大小一致。然而,印刷设计与网页设计一样,不只是网页设计是印刷设计。

在印刷设计中使用点,甚至厘米或盎司,是完全可以接受的,因为打印设备知道其输出区域的物理大小。如果打印机加载了 8.5×11 英寸纸张,那打印机就知道其有一个可以适应纸张边缘的打印区域。它也知道每英寸有多少点,因为它知道自己能够生成的 dpi。这意味着它可以处理像点这样的物理世界长度单位。

许多印刷样式表都是从这里开始的:

body {font: 12pt "Times New Roman", "TimesNR", Times, serif;}

这是如此传统,以至于可能会让站在你旁边的平面艺术家感动流泪。但请确保他们明白,只因为印刷媒介的特性,点阵字才被接受,但它们在网页设计中仍然不合适。

或者,大多数打印输出中缺少背景可能会让设计师感到沮丧。为了节省用户的墨水,大多数网页浏览器预设不打印背景颜色和图像。如果用户希望在打印输出中看到这些背景,则必须更改偏好设置中的选项。

CSS 无法强制打印背景。但是,您可以使用打印样式表使背景变得不必要。例如,您可以在打印样式表中包含此规则:

* {color: black !important; background: transparent !important;}

这将尽最大努力确保所有元素打印为黑色文本,并删除您可能在全媒体样式表中分配的任何背景。它还确保,如果您的网页设计将黄色文本放在深灰色背景上,那么使用彩色打印机的用户将不会在白纸上看到黄色文本。

分页媒体和连续媒体之间的另一个区别是,在分页媒体中,多列布局更难使用。假设您有一篇文章,其中的文本格式为两列。在打印输出中,每页的左侧将包含第一列,右侧将包含第二列。这将强迫用户先阅读每页的左侧,然后返回打印输出的开头并阅读每页的右侧。在网络上这已经很烦人了,但在纸上更加糟糕。

一个解决方案是使用 CSS 来布局您的两列(例如使用 flexbox),然后编写一个打印样式表,将内容恢复为单列。因此,您可以像这样编写屏幕样式表:

article {display: flex;}
div#leftcol {flex: 0 0 45%;}
div#rightcol {flex: 0 0 5 45%;}

然后在您的打印样式表中,您会编写以下内容:

article {display: block; width: auto;}

或者,在支持的用户代理中,您可以为屏幕和打印定义实际的多列布局,并信任用户代理执行正确的操作。

我们可以花一个完整的章节来讨论打印设计的细节,但这并不是本书的目的。让我们开始探讨分页媒体 CSS 的细节,并将设计讨论留给另一本书。

页面尺寸

与定义元素框相同的方式定义了一个页面框,描述了页面的组成部分。页面框由两个主要区域组成:

页面区域

内容布局的页面部分。这在某种程度上类似于普通元素框的内容区域,以至于页面区域的边缘充当页面内布局的初始包含块。

边距区域

围绕页面区域的区域。

图 21-2 展示了页面框模型。

css5 2102

图 21-2. 页面框

@page 块是进行设置的方法,size 属性用于定义页面框的实际尺寸。这里有一个简单的示例:

@page {size: 7.5in 10in; margin: 0.5in;}

@page 就像 @media 一样是一个块,它可以包含任何一组样式。其中之一,size,只在 @page 块的上下文中才有意义。

警告

截至 2022 年末,只有基于 Chromium 的浏览器支持 size

此描述符定义了页面区域的大小。landscape 的值旨在使布局旋转 90 度,而 portrait 是西方语言印刷的正常方向。因此,您可以通过声明以下内容来使文档侧向打印,结果如图 21-3 所示:

@page {size: landscape;}

css5 2103

图 21-3. 横向页面尺寸

除了 landscapeportrait 外,还有预定义的页面尺寸关键字可用。这些在表 21-1 中总结。

表 21-1. 页面尺寸关键字

关键字 描述
A5 国际标准 ISO A5 尺寸,148 毫米宽 x 210 毫米高 (5.83 英寸 x 8.27 英寸)
A4 国际标准 ISO A4 尺寸,210 毫米 x 297 毫米 (8.27 英寸 x 11.69 英寸)
A3 国际标准 ISO A3 尺寸,297 毫米 x 420 毫米 (11.69 英寸 x 16.54 英寸)
B5 国际标准 ISO B5 尺寸,176 毫米 x 250 毫米 (6.93 英寸 x 9.84 英寸)
B4 国际标准 ISO B4 尺寸,250 毫米 x 353 毫米 (9.84 英寸 x 13.9 英寸)
JIS-B5 ISO 日本工业标准(JIS)B5 尺寸,182 毫米 x 257 毫米 (7.17 英寸 x 10.12 英寸)
JIS-B4 ISO JIS B4 尺寸,257 毫米 x 364 毫米 (10.12 英寸 x 14.33 英寸)
letter 北美信纸尺寸,8.5 英寸 x 11 英寸 (215.9 毫米 x 279.4 毫米)
legal 北美法律尺寸,8.5 英寸 x 14 英寸 (215.9 毫米 x 355.6 毫米)
ledger 北美分类账尺寸,11 英寸 x 17 英寸 (279.4 毫米 x 431.8 毫米)

可以使用任何一个关键字来声明页面尺寸。以下定义了一个 JIS B5 尺寸的页面:

@page {size: JIS-B5;}

这些关键字可以与 landscapeportrait 关键字结合使用;因此,要定义横向布局的北美法律页面,可以使用以下内容:

@page {size: landscape legal;}

除了使用关键字,也可以使用长度单位定义页面尺寸。首先给出宽度,然后是高度。因此,以下定义了一个 8 英寸宽、10 英寸高的页面区域:

@page {size: 8in 10in;}

定义的区域通常居中于物理页面内,四周有相等的空白区域。如果定义的 size 大于页面的可打印区域,用户代理程序必须决定如何解决这种情况。这里没有定义的行为,所以完全由实现者选择。

页面边距和内边距

相关于 size,CSS 包括了对页面盒子边距区域进行样式化的功能。如果你希望确保每张 8.5 × 11 英寸页面只使用中心的一个小部分进行打印,可以这样写:

@page {margin: 3.75in;}

这将留下一个 1 英寸宽、3.5 英寸高的打印区域。

理论上可以使用 emex 作为长度单位来描述边距区域或页面区域。所使用的尺寸取自页面上下文的字体,也就是页面上显示的内容的基础字体大小。

命名的页面类型

CSS 允许您使用命名的@page规则创建不同的页面类型。比如,您有一份关于天文学的长文档,在其中一个地方,一个相当宽的表格包含了所有土星卫星的物理特性列表。您希望将文本打印为竖排模式,但表格需要横排模式。以下是您的起始点:

@page normal {size: portrait; margin: 1in;}
@page rotate {size: landscape; margin: 0.5in;}

现在您只需根据需要应用这些页面类型即可。土星的卫星表具有moon-dataid,因此您可以编写以下规则:

body {page: normal;}
table#moon-data {page: rotate;}

这会导致表格以横向打印,但文档的其余部分以纵向打印。page属性使这一操作成为可能。

从值定义中可以看出,page的存在完全是为了让您能够将命名的页面类型分配给文档中的各种元素。

通过特殊的伪类可以使用更通用的页面类型。:first页面伪类允许您在文档中的第一页应用特殊样式。例如,您可能希望给第一页的顶部边距比其他页面更大。以下是具体操作:

@page {margin: 3cm;}
@page :first {margin-top: 6cm;}

这将在所有页面上产生 3 厘米的边距,除了第一页的顶部边距为 6 厘米。

除了对第一页进行样式设置之外,还可以对左右页进行样式设置,模拟书籍书脊左右的页。您可以使用:left:right对它们进行不同的样式设置。例如:

@page :left {margin-left: 3cm; margin-right: 5cm;}
@page :right {margin-left: 5cm; margin-right: 3cm;}

这些规则将在左右页面的内容之间放置更大的边距,这些边距位于书脊所在的侧面。这是将页面绑定到某种类型的书籍中时的常见做法。

警告

早在 2023 年初,Firefox 系列就不支持:first:left或者:right

分页

在分页媒体中,通过使用page-break-beforepage-break-after属性可以对页面分页的位置产生影响。这两个属性接受相同的一组值。

auto的默认值意味着在元素之前或之后不会强制插入分页符。这与普通打印一样。always值会导致在样式化元素之前(或之后)插入分页符。

例如,假设页面标题是一个<h1>元素,而各节标题都是<h2>元素。我们可能希望在文档的每个部分之前和文档标题之后插入分页符。这将导致以下规则,如图 21-4 所示:

h1 {page-break-after: always;}
h2 {page-break-before: always;}

css5 2104

图 21-4. 插入分页符

如果我们希望文档标题居中显示在页面上,我们会添加相关规则。因为我们不需要这样做,所以每页都会直接呈现。

leftright 的值的操作方式与以往相同,只是它们进一步定义了可以恢复打印的页面类型。考虑以下内容:

h2 {page-break-before: left;}

这将导致每个<h2>元素前都插入足够多的分页,以便将其打印在左页的顶部——即输出绑定时,看起来是书脊左侧的页面表面。在双面打印中,这意味着在一张纸的背面打印。

因此,假设在打印时,紧挨着一个<h2>的元素打印在右页上。前述规则将导致在<h2>之前插入一个分页,从而将其推到下一页。然而,如果下一个<h2>前面是左页上的元素,则<h2>前面将插入两个分页,从而使其位于下一个左页的顶部。两者之间的右页将被有意留空。right值具有相同的基本效果,只不过它会强制将元素打印在右页的顶部,并且之前可能会有一到两个分页。

always的伴侣是avoid,它指示用户代理尽量避免在元素之前或之后插入分页。延续上一个例子,假设您有标题为<h3>元素的子节。您希望保持这些标题与其后的文本在一起,因此希望尽可能避免在<h3>之后插入分页:

h3 {page-break-after: avoid;}

注意,这里所说的值被称为avoid,而不是never。绝对不能保证在给定元素之前或之后永远不插入分页。考虑以下情况:

img {height: 9.5in; width: 8in; page-break-before: avoid;}
h4 {page-break-after: avoid;}
h4 + img {height: 10.5in;}

现在,进一步假设<h4>标签放置在两张图片之间,并且其高度计算为半英寸。每张图片必须单独打印在一页上,但是<h4>标签只能放在两个地方之一:在包含第一个元素的页面底部,或者在其后的页面上。如果它放在第一张图片之后,则必须跟随分页,因为没有足够的空间让第二张图片跟随它。

另一方面,如果<h4>标签放在第一张图片后的新页面上,那么在同一页上就无法为第二张图片腾出位置。因此,再次,在<h4>标签后会发生分页。而且,在任何情况下,至少一张图片,如果不是两张,都会在分页之前出现。在这种情况下,用户代理能做的事情有限。

虽然这类情况很少见,但它们确实会发生——例如在只包含表格和标题之前的文档中。表格可能以一种强制要求标题元素后跟分页的方式打印,尽管作者请求避免此类分页位置。

其他page-break-inside属性可能出现的问题与上述类似。它的可能值比其姐妹更有限。

使用 page-break-inside,除了默认之外,你基本上只有一个选项:请求用户代理尝试避免在元素内部放置页面断页。如果你有一系列aside分区,并且不希望它们跨两页分割,那么可以声明如下:

div.aside {page-break-inside: avoid;}

再次强调,这只是一个建议,而不是一个实际的规则。如果一个aside内容超过一页,用户代理无法避免在元素内部插入页面断页。

孤行和孤字

传统印刷排版和桌面出版都具有影响页面断页的两个共同属性:widowsorphans

这些属性有着类似的目标,但从不同角度来实现它们。widows的值定义了元素中可以放置在页面顶部而不强制在元素之前插入页面断页的最小行框数。orphans属性则具有相反的效果:它给出了可以出现在页面底部而不强制在元素之前插入页面断页的最小行框数。

让我们以 widows 为例。假设你声明如下:

p {widows: 4;}

这意味着任何段落顶部至少会有四行框出现在页面上。如果文档布局导致行框数不足,则整个段落将置于页面顶部。

考虑 图 21-5 中的情况。用手遮住图的顶部部分,只显示第二页。注意那里有两个行框,来自前一页开始的段落的结尾。默认的widows值为2,这是一种可以接受的渲染。但是,如果值为3或更高,则整个段落将作为一个单独的块出现在第二页的顶部。这将要求在相关段落之前插入页面断页。

css5 2105

图 21-5. 计算孤行和孤字

参考 图 21-5,这次用手遮住第二页。注意页面底部的四行框,在最后一段的开头。只要orphans的值为4或更少都可以接受。如果是5或更高,则段落会再次在页面之间断开,并作为第二页顶部的单个块来布局。

一个潜在的问题是必须同时满足orphanswidows。如果声明如下,则大多数段落将没有内部页面断页:

p {widows: 30; orphans: 30;}

给定这些值,需要一个相当长的段落才能允许内部页面断页。如果意图是防止内部分页,那么最好表达为以下形式:

p {page-break-inside: avoid;}
警告

大多数浏览器长期以来一直支持widowsorphans,但是到 2023 年初为止,Firefox 系列似乎仍然不支持它们。

页面断页行为

因为 CSS 允许一些奇怪的页面断点样式,它定义了一套关于允许页面断点和“最佳”页面断点的行为。这些行为指导用户代理如何在不同情况下处理页面断点。

页面断点只允许出现在两个通用位置。其中之一是在两个块级框之间。如果页面断点出现在两个块框之间,页面断点前的元素的margin-bottom值将被重置为0,并且页面断点后的元素的margin-top也将被重置为0。然而,有两条规则影响页面断点是否可以出现在两个元素框之间:

  • 如果第一个元素的page-break-after值或第二个元素的page-break-before值是alwaysleftright,则会在元素之间放置页面断点。这是无论另一个元素的值如何(即使为avoid),都成立的情况。(这是强制页面断点。)

  • 如果第一个元素的page-break-after值是auto,并且第二个元素的page-break-before值也是auto,并且它们没有共享一个祖先元素,该祖先元素的page-break-inside值不是avoid,那么页面断点可以放置在它们之间。

图 21-6 展示了在假设文档中各元素之间可能的页面断点放置。强制页面断点显示为实心方块,而潜在(非强制)页面断点则显示为开放方块。

其次,页面断点允许在块级元素框内的两个行框之间。这也受到一对规则的控制:

  • 只有当从元素开始到页面断点前的行框之间的行框数量少于元素的orphans值时,页面断点才可能出现在两个行框之间。类似地,只有当从页面断点后的行框到元素结束之间的行框数量少于元素的widows值时,页面断点才可以放置。

  • 如果元素的page-break-inside值不是avoid,则可以在行框之间放置页面断点。

css5 2106

图 21-6. 块框之间的潜在页面断点放置

在两种情况下,如果没有页面断点可以满足所有规则,则忽略控制页面断点放置的第二条规则。因此,如果一个元素已经设置了page-break-inside: avoid,但该元素长度超过一页,页面断点将允许在该元素内的两个行框之间发生。换句话说,忽略了关于行框之间页面断点放置的第二条规则。

如果在每一对规则中忽略第二条规则仍不能产生良好的页面断点放置,还可以忽略其他规则。在这种情况下,用户代理可能会忽略所有页面断点属性值,并按照它们全部为auto的方式进行处理,尽管这种方法未在 CSS 规范中定义(或要求)。

除了前面探讨过的规则外,CSS 还定义了一组最佳页面断开行为:

  • 尽量少断开。

  • 使所有不以强制断开结尾的页面看起来大约具有相同的高度。

  • 避免在具有边框的块内部断开。

  • 避免在表格内部断开。

  • 避免在浮动元素内部断开。

这些建议并非用户代理的强制要求,但它们提供了应该导致理想页面断开行为的逻辑指导。

重复元素

在分页媒体中非常常见的需求是能够有 running head 的能力。这是出现在每页上的元素,例如文档标题或作者姓名。在 CSS 中,可以通过使用固定位置元素来实现:

div#runhead {position: fixed; top: 0; right: 0;}

当文档输出到分页媒体时,将把任何带有 idrunhead<div> 放置在每个页面框的右上角。相同的规则会将该元素放置在连续媒体(如 Web 浏览器)的视口右上角。以这种方式定位的任何元素都会出现在每一页上。不可能复制一个元素以成为重复元素。因此,根据以下情况,<h1> 元素将作为 running head 出现在每一页上,包括第一页:

h1 {position: fixed; top: 0; width: 100%; text-align: center;
    font-size: 80%; border-bottom: 1px solid gray;}

缺点是 <h1> 元素因位于第一页而只能打印为 running head。

最终,我们将能够通过 @page 的页边规则直接向打印页面的页边添加内容。以下示例将“目录”放置在包含设置了 page: toc 的元素的打印页面的顶部中间位置:

@page toc {
    size: a4 portrait;
    @top-middle {
        content: "Table of contents";
    }
}

页面外的元素

所有关于在分页媒体中定位元素的讨论引发了一个有趣的问题:如果一个元素定位在页面框之外会发生什么?甚至无需定位即可创建这种情况。考虑一个包含 411 个字符的 <pre> 元素的行。这可能比任何标准纸张都宽,因此该元素将比页面框更宽。那么会发生什么呢?

事实证明,CSS 并没有确切规定用户代理应该采取什么行动,因此每个用户代理都需要自行解决。对于非常宽的 <pre> 元素,用户代理可能会裁剪元素到页面框并丢弃其余内容。它也可以生成额外的页面来显示元素剩余的部分。

CSS 对处理超出页面框之外的内容有一些一般性建议,其中两条非常重要。首先,内容应允许略微伸出页面框以便出血。这意味着对于超出页面框但未完全延伸至页面外的内容部分,不会生成额外页面。

第二,建议用户代理不要仅为了遵守定位信息而生成大量空白页面。请考虑以下内容:

h1 {position: absolute; top: 1500in;}

假设页面框高度为 10 英寸,则用户代理必须在<h1>之前添加 150 个页面断点(因此有 150 个空白页),以遵守该规则。但是,用户代理可能会选择跳过空白页,仅输出最后一个包含<h1>元素的页面。

规范中的另外两个建议指出,用户代理不应将元素定位在奇怪的位置,仅仅是为了避免渲染它们,并且放置在页面框之外的内容可以以多种方式渲染。(CSS 中的一些评论是有用和引人入胜的,但有些似乎仅仅是为了愉快地陈述显而易见的事实。)

容器查询

就像媒体查询适用于媒体上下文一样,容器查询适用于容器上下文。与其说是因为显示尺寸变化而要改变设计的某一部分布局,不如说是可以通过更改其父元素的尺寸来实现这些变化。

例如,您可能有一个包含标志、一些导航栏链接和搜索框的页面头部。默认情况下,搜索框较窄,以免占用太多空间。然而,一旦获得焦点,它会变得更宽。在这种情况下,您可能希望更改标志和链接的布局和大小,从而让出搜索框,而不是完全消失或被覆盖。以下是设置方法:

<header id="site">
  <nav>
    <a href="…"><img src="/i/logo.png" alt="ConHugeCo"></a>
    <a href="…">Products</a>
    <a href="…">Services</a>
    <!-- and so on -->
  </nav>
  <form>
    <!-- search form is here -->
  </form>
</header>
header#site nav {container: headernav / size;}

@container headernav (width < 50%) {
	/* style changes to be applied to elements when the nav element
 shrinks in inline size below half-width */
}

让我们探索容器查询引入的新属性,然后深入查询块语法。

警告

容器查询在 2022 年中期到晚期获得了广泛的浏览器支持,因此如果您的用户使用早于此的浏览器,请注意使用时的情况。尽管如此,容器查询在所有主流浏览器中都受支持。

定义容器类型

有几种方法可以定义容器的类型,同时设置容器启用的内容(请参见contain中的第二十章)。所有这些都通过container-type属性进行管理。

当使用默认值normal时,可以查询容器在特定属性值组合上的情况。假设您希望在容器具有特定边距值时应用某些样式。那么看起来会像这样:

header#site nav {
	container-type: normal; /* default value */
	container-name: headernav;
}

@container headernav style(padding-inline: 1em) {
	/* style changes to be applied to elements when the nav element
 specifically has 1em inline padding, and no other value(s) */
}

style()函数中,可以使用任何属性和值组合,包括涉及自定义属性的组合,只要该精确组合有效即可。例如,您可以根据文本大小调整自定义属性的值来更改标题文本的颜色:

main > section {
	container: pagesection / normal;
}

@container pagesection style(--textSize: x-small) {
	h1, h2, h3, h4, h5, h6 {color: black;}
}
@container pagesection style(--textSize: normal) {
	h1, h2, h3, h4, h5, h6 {color: #222;}
}
@container pagesection style(--textSize: x-big) {
	h1, h2, h3, h4, h5, h6 {color: #444;}
}

您还可以查询特定的尺寸值,例如(width: 30em),但这仅查询 CSS 属性的值,而不是容器的渲染大小。如果要执行基于范围的大小查询,您将需要使用container-type的其他值之一:sizeinline-size

如果你声明 container-type: size,那么你可以在内联和块轴上进行查询。因此,你可以像这样设置一个涉及容器大小的查询:

header#site nav {
	container-type: size
	container-name: headernav;
}

@container headernav (block-size < 6rem) and (inline-size < 50vmin) {
	/* style changes to be applied to elements when the nav element
 has a block size below 6rem AND an inline size below 50vmin */
}

如果你只关心内联尺寸,那么使用 inline-size 可能更合理,如下所示:

header#site nav {
	container-type: inline-size
	container-name: headernav;
}

@container headernav (inline-size => 50vmin) {
	/* style changes to be applied to elements when the nav element
 has an inline size greater than or equal to 50vmin */
}

除了其中一个允许块轴查询之外,实际区别是什么?这两个值都设置了布局和样式的包含性(参见第二十章中的 contain 属性),但 size 设置了尺寸包含性,而 inline-size 设置了内联尺寸包含性。考虑到它们各自的名称,这显得很合理。如果你总是只进行内联查询,使用 inline-size 可以保持块方向不受限制。

在本节中,我们一直在设置一个容器名称,但实际上并没有真正讨论它,现在让我们来谈谈它。

定义容器名称

要引用一个容器,该容器需要一个名称,这就是 container-name 提供的功能。它甚至允许你给同一个元素分配多个名称。

几乎任何时候,你设置一个容器,都应该设置一个容器名称或者多个名称。以下两条规则都是合法的:

header {container-name: pageHeader;}
footer {container-name: pageFooter full-width nav_element;}

好吧,你可能不应该混合使用驼峰命名、连字符分隔和下划线分隔的命名约定,但其他方面都没问题。<header> 元素将被赋予容器名称 pageHeader,而 <footer> 元素将被赋予列出的所有三个容器名称。这使你可以为不同的事物应用不同的容器查询,如下所示:

@container pageFooter (width < 40em) {
	/* rules for elements in narrow footers go here */
}
@container nav_element (height > 5rem) {
	/* rules for elements in tall elements that contain navigation go here */
}
@container full-width style(border-style: solid) {
	/* rules for elements in full-width containers go here */
}

我们可以反过来,给一堆元素分配相同的容器名称:

header#page, .full-width, full-bleed, footer {
	container-name: full-width;
}

@container full-width style(border-style: solid) {
	/* rules for elements in full-width containers go here */
}

使用容器简写

现在让我们将这两个属性合并到一个简写中,container

如果你想要在一个便捷的声明中定义容器名称和类型,这个属性就适合你。例如,以下两条规则是完全等价的:

header#page nav {
	container-name: headerNav;
	container-type: size;
}
header#page nav {
	container: headerNav / size;
}

container 值中,名称必须始终存在,并且必须首先出现。如果定义了容器类型,则必须在斜杠 (/) 后面出现。如果没有给出容器类型,则使用 normal 的初始值。因此,以下规则是完全等价的:

footer#site nav {
	container-name: footerNav;
	container-type: normal;
}
footer#site nav {
	container: footerNav / normal;
}
footer#site nav {
	container: footerNav;
}

container-name 一样,你可以包含一个空格分隔的名称列表,如下所示:

footer#site nav {
	container: footerNav fullWidth linkContainer / normal;
}

所以这些都是设置容器名称和类型的方式。你已经看到了 @container 块用于调用这些内容,现在是时候讨论它的工作原理了。

使用容器规则

容器查询块的语法如果你读过早期关于媒体查询的部分,那么容器查询块的语法会显得很熟悉,因为语法几乎相同。唯一的真正区别是容器查询使用可选的容器名称和 style() 函数。以下是基本的语法格式:

@container *`<container-name>`*? *`<container-condition>`* {
	/* CSS rules go here */
}

您不必包含容器名称,但如果包含,它必须放在第一位。(我们将很快讨论如果不包含会发生什么。)但是,必须有某种条件——某种查询条件。毕竟,如果没有条件,它就不会是一个容器查询。

与媒体查询类似,您可以使用andnotor修饰符来设置查询。假设您想匹配一个没有虚线边框的容器。具体操作如下:

@container not style(border-style: dashed) {
	/* CSS rules go here */
}

或许你希望在名为fullWidth的容器处于某个大小范围内且没有虚线边框时应用某些规则:

@container fullWidth (inline-size > 30em) and not style(border-style: dashed) {
	/* CSS rules go here */
}

请注意,您可以列出仅一个容器名称;无法将它们在单个查询块中组合,无论是逗号还是逻辑组合器如and。尽管如此,您可以嵌套容器查询,例如以下示例:

@container fullWidth (inline-size > 30em) and not style(border-style: dashed) {
	@container headerNav (inline-size > 30em) {
		/* CSS rules go here */
	}
}

当元素具有具有fullWidth容器(其内联尺寸大于 30 em 且没有虚线边框样式)和具有内联尺寸大于 30 em 的headerNav容器时,将匹配并应用样式。同一个元素可能同时是这两种容器!

这引出了一个问题,即一个元素如何确切地知道正在查询哪些容器。让我们稍微扩展之前的例子并填写实际的 CSS 规则:

@container fullWidth (inline-size > 30em) and not style(border-style: dashed) {
	nav {display: flex; gap: 0.5em;}
}

页面上的给定<nav>元素如何知道它何时被容器查询匹配?通过查找其祖先树,看看是否有任何容器位于其上方。如果有,并且它们与容器块中出现的名称匹配,并且指定的查询与容器类型匹配,则进行查询。如果返回 true,则应用容器块中的样式。让我们看看它是如何运作的。这是一个文档骨架:

html
  body
    header.page
      img
      nav
        (links here)
    main
      h1
      aside
        nav
      p
      p
      p
      p
    footer.page
      nav
        (links here)
      img

对于这些标记,我们将应用以下样式:

header.page {container: headerNav fullWidth / size;}
footer.page {container: fullWidth / size;}
body, main {container-type: normal;}

nav {display: flex; gap: 0.5em;}

@container fullWidth (inline-size < 30em) {
	nav {flex-direction: column; padding-block: 4em;}
}
@container headerNav (block-size > 25vh) {
	nav {font-size: smaller; padding-block: 0; margin-block: 0;}
}
@container style(background-color: blue;) {
	nav {color: white;}
	nav a {color: inherit; font-weight: bold;}
}

在标记中,我们有三个<nav>元素,在 CSS 中我们有三个容器块。让我们逐一考虑这些块。

第一个容器查询块对所有<nav>元素说:“如果你有一个名为fullWidth的容器,并且该容器的内联尺寸小于 30 em,则应用这些样式。”页眉和页脚的<nav>元素确实具有名为fullWidth的容器:<header><footer>元素都有这个名称。它们的容器类型也是size,因此检查内联尺寸是有效的。因此,它们检查其各自容器的内联尺寸,以确定是否应用样式。

注意,这是每个容器分别进行的。页眉可能是 40 em 宽,页脚因为其他布局样式(例如网格模板)而只有 25 em 宽。在这种情况下,flex 方向的更改将应用于页脚的<nav>,但不会应用于页眉的<nav>。至于<main>元素内的<nav>,因为没有任何被标记为fullWidth的容器,所以不管条件查询如何,都会被跳过。

第二个容器查询块对所有<nav>元素说:“如果您有一个名为headerNav的容器,并且该容器的块大小大于 25 vh,则应用这些样式。”页面上唯一一个名为headerNav的容器是<header class="page">,因此其<nav>检查容器的块大小,如果容器的块大小超过 25 vh,则应用样式。其他两个<nav>元素完全跳过此查询,因为它们的容器没有命名为headerNav

第三个容器查询块更加模糊。它对所有<nav>元素说:“如果您有一个容器且其背景为蓝色,则应用这些样式。”注意没有容器名称,所以头部<nav>检查其最近的祖先容器header.page,看它是否设置为background-color: blue。假设它没有,因此这些样式不会应用。

同样的事情发生在<main>和页脚内部的<nav>及其中的任何<a>元素。我们已经在前面的段落中确定,其背景颜色不是蓝色,因此如果<main>或页脚的背景颜色设置为blue,那么它们各自的<nav>元素和链接将获得这些样式;否则,它们将不会获得。

记住,只有当元素匹配查询块内的选择器时,容器查询才会起作用。想象有人写了这样的内容:

@container (orientation: portrait) {
	body > main > aisde.sidebar ol li > ul li > ol {
		display: flex;
	}
}

只有匹配那些长而非常具体的选择器的元素才能检查其容器,查看它们是否处于portrait方向,即使匹配选择器的元素如果没有任何容器也不会获得样式。否则,查询就有点没有意义。这说明在担心查询任何容器之前,确保您的选择器将匹配,并确保您匹配的元素具有可查询的容器的必要性。

定义容器查询特性

您可以在容器查询中检查七个特性,其中大多数您之前已经看过,但还有一些我们尚未涉及的。它们在这里总结如下:

特性:block-size

值:<length>

查询查询容器内容框的块大小。

特性:inline-size

值:<length>

查询查询容器内容框的内联尺寸。

特性:width

值:<length>

查询查询容器内容框的物理宽度。

特性:height

值:<length>

查询查询容器内容框的物理高度。

特性:aspect-ratio

值:<ratio>

查询查询容器内容框的物理宽度与高度之比。

特性:orientation

值:portrait | landscape

查询查询容器内容框的物理宽度和高度。如果容器的宽度大于其高度,则认为容器是landscape;否则,认为容器是portrait

这些没有min-max-前缀的变体。而是使用我们之前介绍过的数学样式范围表示法。

设置容器长度单位

除了查询容器之外,您还可以根据其容器的大小为元素设置基于长度的样式值,这与第五章中讨论的视口相关长度单位非常相似。具体如下:

cqb

容器的块大小的 1%。

cqi

容器的内联尺寸的 1%。

cqh

容器的物理高度的 1%。

cqw

容器的物理宽度的 1%。

cqmin

等同于cqbcqi,以较小者为准。

cqmax

等同于cqbcqi,以较大者为准。

因此,您可以设置一个元素,使得在较小的容器尺寸下,其子元素是容器的全宽度,但在较大尺寸下它们是容器宽度的某个分数。例如,可以使用网格轨道实现:

div.card {
	container: card / inline-size;
}

@container card (width > 45em) {
	div.card > ul {
		display: grid;
		grid-template-columns: repeat(3, 30cqw);
		justify-content: space-between;
	}
}

在这里,如果容器的宽度超过 45 em,div.card的子元素 <ul> 将被转换为网格容器,并且列的大小基于容器的宽度。见图 21-7。

css5 2107

图 21-7. 使用容器查询单位

这里的优势主要体现在像 Web 组件这样的应用中,希望根据容器的大小调整元素大小,即使容器可能以多种尺寸条件出现。

特性查询(@supports)

CSS 具有在用户代理支持特定 CSS 属性值组合时应用规则的能力。这些被称为特性查询

假设您希望仅当color是受支持的属性时才向元素应用颜色。 (这当然应该是!)那将看起来如下所示:

@supports (color: black) {
    body {color: black;}
    h1 {color: purple;}
    h2 {color: navy;}
}

这实际上是在说,“如果您识别并能够处理属性值组合color: black,则应用这些样式。否则,跳过这些样式。”在不理解@supports的用户代理中,整个块都会被跳过。

特性查询是逐步增强您样式的完美方式。例如,假设您想要在现有的浮动和内联块布局中添加一些网格布局。您可以保留旧的布局方案,然后在样式表中稍后包含如下块:

@supports (display: grid ) {
    section#main {display: grid;}
    /* styles to switch off old layout positioning */
    /* grid layout styles */
}

在理解网格显示的浏览器中将应用这些样式,覆盖了管理页面布局的旧样式,并应用使事物在基于网格的未来中工作所需的样式。对于过于老旧而不理解网格布局的浏览器来说,它们将完全跳过整个块,就像它从未存在过一样。

特性查询可以嵌套在彼此内部,事实上也可以嵌套在媒体块内部,反之亦然。您可以基于弹性盒布局编写屏幕和打印样式,并将这些媒体块包装在@supports (display: flex)块中:

@supports (display: flex) {
    @media screen {
        /* screen flexbox styles go here */
    }
    @media print {
        /* print flexbox styles go here */
    }
}

相反,您可以在各种响应式设计媒体查询块内部添加 @supports() 块:

@media screen and (max-width: 30em){
    @supports (display: flex) {
        /* small-screen flexbox styles go here */
    }
}
@media screen and (min-width: 30em) {
    @supports (display: flex) {
        /* large-screen flexbox styles go here */
    }
}

如何组织这些块完全取决于您。对于容器查询也是如此,它们可以嵌套在功能查询内部,反之亦然。实际上,您可以将各种类型的查询嵌套在彼此内部,或者自己内部,以任何对您的情况(以及对您自己)有意义的组合方式。

与媒体查询一样,功能查询也允许逻辑运算符。假设我们只想在用户代理支持网格布局 CSS 形状时应用样式。可能会是这样的:

@supports (display: grid) and (shape-outside: circle()) {
    /* grid-and-shape styles go here */
}

这本质上等同于编写以下内容:

@supports (display: grid) {
    @supports (shape-outside: circle()) {
        /* grid-and-shape styles go here */
    }
}

不过,并不仅限于“和”操作可用。CSS 形状(在 第二十章 中详细介绍)就是“或”非常有用的一个例子,因为长期以来 WebKit 仅通过供应商前缀属性支持 CSS 形状。因此,如果要使用形状,可以使用如下特性查询:

@supports (shape-outside: circle()) or
          (-webkit-shape-outside: circle()) {
        /* shape styles go here */
}

您仍然必须确保同时使用形状属性的前缀和无前缀版本,但这样可以在支持形状的其他浏览器中向后添加对这些属性的支持,同时支持 WebKit 发布线上的前缀版本。

所有这些都很方便,因为有时您可能希望应用不同于您正在测试的属性。因此,回到网格布局,您可能希望在使用网格时更改布局元素的边距等。这是该方法的简化版本:

div#main {overflow: hidden;}
div.column {float: left; margin-right: 1em;}
div.column:last-child {margin-right: 0;}

@supports (display: grid) {
    div#main {display: grid; gap: 1em 0;
            overflow: visible;}
    div#main div.column {margin: 0;}
}

也可以使用否定。例如,当不支持网格布局时,您可以应用以下样式:

@supports not (display: grid) {
    /* grid-not-supported styles go here */
}

您可以将逻辑运算符组合成单个查询,但需要使用括号保持逻辑清晰。假设我们想要在支持颜色的同时,支持网格或弹性盒布局之一时应用一组样式。写成这样:

@supports (color: black) and ((display: flex) or (display: grid)) {
        /* styles go here */
}

请注意,在逻辑“或”部分周围还有一组额外的括号,围绕网格和弹性盒测试。这些额外的括号是必需的。如果没有它们,整个表达式将失败,并且块内的样式将被跳过。换句话说,不要 这样做:

/* the following will not work and is a bad idea */
@supports (color: black) and (display: flex) or (display: grid) {

最后,您可能会想知道为什么功能查询测试中需要属性和值。毕竟,如果您正在使用形状,您只需要测试 shape-outside,对吧?这是因为浏览器可以轻松支持一个属性而不支持其所有值。网格布局就是一个完美的例子。假设您尝试像这样测试网格支持:

@supports (display) {
    /* grid styles go here */
}

即使 Internet Explorer 4 也支持 display。任何理解 @supports 的浏览器肯定也理解 display 及其许多值,但可能不包括 grid。这就是为什么属性和值始终在功能查询中进行测试的原因。

警告

记住这些是功能查询,而不是正确性查询。浏览器可以理解您测试的功能,但可能会带有错误实现或者在不实际支持预期行为的情况下正确解析它。换句话说,浏览器不能保证支持正确性。正面的功能查询结果只是表示浏览器理解您说的内容。

其他规则

其他各种规则在本书的其他部分都有涵盖:

  • @counter-style(参见第十六章)

  • @font-face(参见第十四章)

  • @font-feature-values(参见第十四章)

  • @import(参见第一章)

  • @layer(参见第四章)

另外两个在其他地方未涵盖,所以我们会在这里进行讨论。

为样式表定义字符集

@charset 规则是为样式表设置特定字符集的一种方式。例如,您可能会收到一个使用 UTF-16 字符编码的样式表。其标记如下:

@charset "UTF-16";

与 CSS 的其余部分不同,这里的语法非常严格。@charset 和引号的值之间必须有一个正好的空格(这个空格必须是 Unicode 代码点 U+0020 定义的空格),值必须用双引号引起,并且只能使用双引号引起。此外,在 @charset 前不能有任何空格;它必须是行的第一件事。

此外,如果需要包含 @charset,它必须是样式表中的第一件事,早于任何其他规则或常规规则。如果列出了多个 @charset,将使用第一个,其余的将被忽略。

最后,唯一可接受的值是互联网号码分配机构(IANA)注册表中定义的字符编码。

使用 @charset 是非常罕见的,所以除非明确声明特定样式表的编码是必需的才能使其工作,否则不用担心它。

为选择器定义命名空间

@namespace 规则允许您在样式表中使用 XML 命名空间。@namespace 的值是定义命名空间的文档的 URL,例如:

<style>
@namespace xhtml url(http://www.w3.org/1999/xhtml);
@namespace svg url(http://www.w3.org/2000/svg);

xhtml|a {color: navy;}
svg|a {color: red;}
a {background: yellow;}
</style>

根据之前的 CSS,在 XHTML 中,<a> 元素将是蓝色底黄色,而在 SVG 中则是红色底黄色。这就是为什么没有命名空间的选择器可以在所有标记语言中通用的原因:没有命名空间意味着没有限制。

任何 @namespace 规则必须在任何 @charset@import 规则之后,但在任何其他样式表内容之前,无论是其他规则还是普通规则。@namespace 规则很少在测试页面之外使用,但如果需要使用它,功能是存在的。

摘要

由于 at-rules 的灵活性,可以在单一样式集中提供多样化的设计体验。无论是重新组织页面以适应不同的显示尺寸,调整颜色方案以支持灰度打印,还是基于包含它们的元素重新设计内容,您都有能力大幅改善您的工作,使其达到最佳状态。

附录 A. 额外资源

这里是一小部分有用的网站、资源和文档,供所有人免费使用:

Can I Use 支持表格 HTML5、CSS3 等

一个可以查找 HTML、CSS 和 JavaScript 中几乎所有内容的最新支持状态的地方。在你需要支持旧版浏览器或查看你最喜欢的最前沿 CSS 特性的实现状态时非常有用。

Mozilla 开发者网络(MDN)

通常被称为“网络开发人员手册”的 MDN 记录并提供几乎每个 Web API 的支持信息 — HTML、CSS、JavaScript、SVG、XML 等等。所有 CSS 相关内容的中心都可以在https://developer.mozilla.org/en-US/docs/Web/CSS找到。

用于癫痫和身体反应的 Web 无障碍性

详细介绍了设计中可能会触发的各种障碍,如果你过度使用动画、视差滚动、闪烁颜色等功能,可能会引发的各种障碍。对于所有新的网页设计师和开发者来说,这都应该是必读的内容。

CSS SpecifiFISHity

一张使用可爱的鱼类(和浮游生物)以及偶尔的鲨鱼来说明特异性的图表。适合打印并挂在你的显示器旁边!

Color.js

一个 JavaScript 库,为高级 CSS 颜色语法提供支持。它包含几种有用的 JavaScript 方法,包括计算两种颜色之间的中点。如果你经常处理颜色操作,这值得一试。

Arkandis 数字铸造厂

一套免费的网页字体,可供个人项目使用。这是 SwitzeraADF 的来源,SwitzeraADF 在第十四章中有详细提及。

Font Squirrel 网页字体生成器

一个在线工具,可以将你有权在网络上使用的字体转换为完整的网络字体,并附带正确的@font-face命令,这样你就可以在网页设计中使用这种字体。

Microsoft Typography Registered Features(OpenType 1.9)

所有可用于 OpenType 字体的注册功能。如果你想通过使用font-feature-settings属性调用任何 OpenType 功能,这将非常有用。

单个 div

Lynn Fisher 的插图画廊,其中每幅插图都由单个<div>元素和大量 CSS 组成。值得探索和查看源码,看看 Lynn 是如何用疯狂的天才创造出特定效果的。

CSS conic-gradient() Polyfill

如果你真的想使用圆锥渐变但又需要支持不支持圆锥渐变的老浏览器,这个 polyfill 能够解决你的问题。

立方贝塞尔曲线

创建立方贝塞尔曲线以供动画使用的工具。

缓动函数速查表

一个包含cubic-bezier()值、演示它们操作方式等内容的缓动曲线集合。

颜色对照表

一张显示 148 个 CSS 颜色关键字(如orangeforestgreen)及其在 RGB、HSL 和十六进制表示法中的等效值的表格。截至 2023 年初,尚未包括 HSL、HWB 等更现代的格式。

posted @ 2025-11-09 18:02  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报