CSS生成内容
在CSS中可以使用content
来生成内容,该特性最早是在CSS2规范中引入的,经过多年的发展,现在该规范已经进入到了Level 3版本。在实际开发中,时常能看到开发人员在伪元素::before
和::after
中使用content
为Web添加内容,只不过这些并不是DOM树中的一员,换句话说,可以在不调整HTML的情况下生成图标、图像、文本等。但很多开发人员可能只知道content
添加一个值,事实上它有很多新的特性大家不太了解,接下来这篇文章我们就来和大家探讨这方面的知识。
什么是生成内容
从技术上讲,生成的内容是由CSS在文档树(DOM树)中创建的一个简单抽象(Abstraction)。因此,在实践中,生成的内容只存在于Web文档的布局中。
在Web构建中,最常见的示例就是用content
来生成图标,比如Font Awesome就采用了这方面的技术:
在content
不仅仅是一个值、实际内容和一个替代文本,还可以像下面这样设置两个值:
.element::before {
content: "★" / "Highlighted item";
}
估计有不少朋友都不知道content
还可以这样使用,甚至还有更多不知道的事情。为了让大家能更彻底的了解content
,我们就先从其语法规则开始。
CSS的content
语法规则
CSS的content
属性用来指定在元素或伪元素中渲染的内容。具体的使用规则如下:
content: normal | none | [ <content-replacement> | <content-list> ] [/ [ <string> | <counter> ]+ ]?
如果content
运用于元素上,它只有一个目的:指定元素正常渲染,或者用图像(可通常还有一些相关的alt
文本)替换元素。
如果content
运用于伪元素上,它更强大。它控制元素是否渲染,是否可以用图像替换元素,或者用任意内容(文本和图像)替换元素。
content
取值不同,所起作用也略有不同。
normal
取值为normal
时:
- 对于元素或外距盒(
margin-box
),它的计算值是元素自身的内容 - 对于伪元素
::before
或::after
,它的计算值是none
- 对于
::marker
,它的计算值是其自身normal
none
在元素上,这将阻止将元素的子元素渲染为该元素的子元素,就好像该元素是空的一样。在伪元素上,它会阻止伪元素的创建,就好像设置了dislay: none
一样。
在这两种情况下,元素或伪元素不会生成任何内容。比如说,在伪元素上显式设置了content: none
,那么该会就不会自动插入到DOM中,也不会生成任何内容:
p::before{
content: none
}
p::after {
content: '(End)'
}
<content-replacement>
相当于<image>
。
它会使元素或伪元素成为可替换的元素,用指定的<image>
填充。它的正常内容被抑制且不生成盒子,类似于设置了display: none
。
如果<image>
是一个无效的图像,那么它必须被处理为表示一个内部宽度和高度为0
的图像,并且会填充一个透明黑色。
<content-list>
相当于:
[ <string> | contents | <image> | <counter> | <quote> | <target> | <leader()> ]+
将元素的内容替换为一个或多个与指定值对应的匿名内联框,按指定的顺序。它的正常内容被抑制并且不产生盒子,好像display: none
。
每个值都向元素的内容贡献一个内联框。对于<image>
,这是一个内联匿名替换元素;对于其他的,它是一个匿名的内联文本。
如果<image>
表示一个无效的映像,用户代理必须执行以下操作之一:
- 跳过
<image>
,不为它生成任何东西 - 显示一些图像的指示,比如”损坏的图像“图标
/ [ <string> | <counter> ]+
为元素指定可替换文本。如果省略,该元素则没有指定可替换文本。
仅从语法上来看,太过于空洞,接下来我们主要围绕<content-list>
的值和函数来和大家展开。
<content-list>
值和函数
<content-list>
值在content
中来生成一个或多个匿名内联框填充元素,包括图像、字符串、计数器的值和元素的文本值。在接下来的内容中来列举这方面的各种可能性。
先来看使用content
常做的事情。
通过CSS的伪元素向另一个元素添加一个元素的示例很多,但这有一个重要的前提,那就是content
的值不能是none
和normal
,因为取这两个值不会创建伪元素。也就是说,我们在::before
或::after
的content
传其它值都可以向另一个元素添加一个元素,并且还可以设置相关的样式,达到一些设计上的需要。比如构建一个提示框:
<!-- HTML -->
<span class="tooltip-toggle" aria-label="Sample text for your tooltip!" tabindex="0">
// CSS
.tooltip-toggle {
cursor: pointer;
position: relative;
&::before,
&::after {
color: #efefef;
opacity: 0;
pointer-events: none;
text-align: center;
position: absolute;
}
&::before {
top: -80px;
left: -80px;
background-color: #2B222A;
border-radius: 5px;
color: #fff;
content: attr(aria-label);
padding: 1rem;
text-transform: none;
transition: all 0.5s ease;
width: 160px;
}
&::after {
top: -12px;
left: 9px;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #2B222A;
content: " ";
margin-left: -5px;
width: 0;
}
&:focus::before,
&:focus::after,
&:hover::before,
&:hover::after {
opacity: 1;
transition: all 0.75s ease;
}
}
效果如下:
另一个示例就是打印Web页面的时候打印出链接地址。大家都知道,用户点击Web文档中的超链接可以跳转到指定的页面,但如果在打印Web页面时,如果未输出对应的链接地址,那么链接就没啥用了。所以在编写CSS样式时,通常在打印样式中使用content
和attr()
函数的组合,将链接地址放在超链接边上,一起打印出来:
@media print {
a[href^="http://"]::after,
a[href^="https://"]::after {
content: " (" attr(href) ")";
}
}
另外一个示例就是,在Web构建中需要自定义一些计数器效果:
我们就可以使用content
、counter-reset
、counter-increment
和::before
组合在一起,来构建:
.card {
counter-increment: count;
&::before {
content: counter(count, decimal-leading-zero);
}
}
从上面这几个常用的示例中可以看出来,我们在content
中插入的值有字符串,比如"("
和")"
,也有attr()
引入HTML的元素的属性值,比如attr(href)
,也有counter()
函数引入counter-increment
声明的值,比如counter(count, decimal-leading-zero)
。这些值对应的是<content-list>
中的<string>
、<counter>
等。接下来,我们具体来看<content-list>
中的值和函数的使用和细节。
<string>
给content
指定字符串,将会生成一个匿名的内联框。给content
指定的字符串可以是任意有效的字符串,比如空格符
,普通的文本字符串,Emoji,以及一些特殊字符,比如HTML的实体符。
给content
指定字符串值时,一般会用单引号或双引号括起来,比如:
li::before {
content: "梦开始的地方";
}
li::before {
content: "\2713";
}
在使用content
插入HTML实体符号,可以直接将HTML的实体符当作字符串给content
:
.element::before {
content: '★'
}
也可以通过下面的工具,将HTML实体符转换成能用于CSS的十六进制:
比如上面的★
转换出来的CSS值就是\2605
:
.element::before {
content: '\2605'
}
正如前面提到的,很多IconFont都采用的是这种方案,在content
传递的是Icon对应的特殊字符串,只不过经过编码,更适用于CSS。
在给content
传字符串时可以同时传多个字符串,比如:
li::before {
content: "(" "\2605" ")";
}
你会发现,渲染出来是(★)
。你可能发现了,即使你在多个字符串之间留有空格,但它们会合并。如果你在引入的字符串中有多个空格符,比如下面这个示例:
li::before {
content: '第一章: ' ' ...'
}
渲染出来的结果会将多个空格字符合并成一个,结果是会这样第一章: ...
。
<image>
给content
传<image>
值是指使用url()
引入一张图像或CSS渐变。这个时候创建了一个内联可替换元素。比如:
li::before {
content: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/144736/marker-icon.png');
}
li::before {
content: linear-gradient(to right, #f36, #90f);
}
正如上面示例所示,该方式给content
传的<image>
值有点类似于background-image
属性。但没有类似background-size
这样的属性来控制<image>
的大小。
如果<image>
是个无效的图像,那么这个值什么都不表示,就好像content
中没设置值一样。
body::before {
content: url('https://w#.png')
}
事实上,给content
传<image>
值平时使用较少,很多同学更喜欢给content
传一个空字符串,然后通过给相应的伪元素设置background
相关的属性,来达到引入图片的效果,比如下面两个示例,最终效果是一致的:
body::before,
body::after {
display: inline-flex;
width: 10vh;
height: 10vh;
margin: 2vh;
border: 2px solid blue;
border-radius: 5px;
}
body::before {
content: linear-gradient(to right, #f36, #90f);
}
body::after {
content: '';
background: linear-gradient(to right, #f36, #90f);
}
当你拖动浏览器改变视窗大小时,你会发现::before
(即直接给content
传<image>
)不会随着容器变化而变化,需要自动刷新一次才能正常填充:
另外,content
引入<image>
时,对应伪元素部分样式看上去未生效,比如下面这个示例,添加一个圆角,结果效果看上去并未运用上:
特别声明:为什么会有这样的现象,我也没有查看相关资料,如果你在这方面有经验,欢迎在下面的评论中分享!
<quote>
当我们在HTML中使用<blockquote>
、<q>
和<cite>
时涉及到引号的使用。这几个标签元素基于不同的语言,引号的使用会有不同的样式。
而样式上的处理可以将quotes
和content
的各种*-quote
值结合起来使用,从而正确地使用这些引号的样式。其中quotes
属性可以接受的值:
quotes: auto | none | [<string><string>]+
每个值的具体含义是:
none
:content
属性的open-quote
和close-quote
不产生引号标记,就好像content
设置了no-open-quote
和no-close-quote
auto
:基于元素或其父元素的lang
,客户端会自动选择适合于印刷的引号(quotes
的值)[ <string> <string> ]+
:content
属性的open-quote
和close-quote
取值来自于这个引号列表。第一对(最左)表示引用的最外层,第二对表示嵌入的第一层,等等。用户代理必须根据嵌入的级别应用适当的一对引号
我们回到content
中引入的<quote>
,其对应的值有:
<quote> = open-quote | close-quote | no-open-quote | no-close-quote
open-quote
和close-quote
:这些值将由quotes
属性中的适当字符串替换,并增加(减少)引号的嵌套级别no-open-quote
和no-close-quote
:不插入任何内容,即没有任何引号
我们来看一个简单的示例,
blockquote p::before {
content: open-quote;
}
blockquote p::after {
content: no-close-quote;
}
blockquote p:last-child::after {
content: close-quote;
}
不同的lang
,引号的效果也不同:
再来看一个示例:
:lang(fr) > * {
quotes: "\00AB\2005" "\2005\00BB" "\2039\2005" "\2005\203A"
}
:lang(en) > * {
quotes: "\201C" "\201D" "\2018" "\2019"
}
// 等同于
:lang(fr) > * {
quotes: "« " " »" "‹ " " ›"
}
:lang(en) > * {
quotes: "“" "”" "‘" "’"
}
示例中设置了quotes
属性,以便在所有封印经上正确地使用open-quote
和close-quote
。但上面代码只适用于包含英语、法语或同时包含这两种语言的Web文档。如果不是这两语言,那么客户端会自动来匹配:
在这个示例中,quotes
指定了两对引号样式:
当你在元素中引用方式有嵌套关系时,就能分层次的使用引号样式:
<!-- HTML -->
<p><q>Trøndere gråter når <q>Vinsjan på kaia</q> blir deklamert.</q></p>
<p lang="zh-CN"><q>我想说<q>梦开始的地方</q> 在这里。</q></p>
// CSS
:lang(en) > q {
quotes: '"' '"' "'" "'"
}
:lang(zh-CN) > q {
quotes: "«" "»" "’" "’"
}
q::before {
content: open-quote
}
q::after {
content: close-quote
}
效果如下:
其他
就目前为止,content
还支持attr()
函数引入HTML元素的属性值,比如文章开头的Tooltips示例,另外还支持<counter>
(即counter-reset
、counter-increment
和counter()
)实现自定义计数器。
除此之外,到目前为止还没有得到浏览器支持的有contents
、<target>
、<leader()>
等。如果这几个值得到浏览器的支持,可以帮助我们实现很多有意思的效果,比如leader()
就可以帮助我们轻易的实现书本大纲的效果:
其他的这里就不发过多时间和大家探讨了,因为目前还没有任何浏览器支持,而且就这部分而言,虽然被纳入到了规范当中,但并不代表未来不会有变化。
可访问性
在上面的示例中,我们看到的示例都有一个共性,就是在content
只接受一个属性值。事实上,它还可以接受另一个值(第二个值),该值和前一个值用 /
来分隔。这样做主要是为了给生成的内容提供可访问性。也就是说,CSS的content
生成的内容也应该提供相应的Web可访问性,即生成的内容应该可搜索、可选择和屏幕阅读器能识别到。
我们来看一个简单的示例:
<!-- HTML -->
<a href="#">Go to favorites</a>
// CSS
a::before {
content: "★";
}
这个时候,读屏幕器(比如iOS voiceover,即旁白)会读出黑色星星 Go to favorites link
:
这种体验其实并不太好。不过幸运的是,现在可以在content
使用可替换文本。即前面提到的/
后面的第二个值。这样一来,上面的示例就可以修改成:
a::before {
content: "★" / "Highlighted item";
}
或者
a::before {
content: "★" / "";
}
这个时候,屏幕阅读器就会把可替代文本内容读出来。屏幕阅读器会读出Highlighted item Go to favorites link
。
同样的,content
属性的第二个值(/
后面的那个值)也可以使用attr()
来引用DOM元素中的属性值做为可替换文本:
a::before {
content: "★" / attr(data-star-alt);
}
目前仅Chrome浏览器支持content: '' / ''
这样的模式。在CSS中,和其他新特性类似,我们可以@supports
来做判断。
@supports (content: "x" / "y") {
a::before {
content: "★" / "Highlighted Text";
}
}
@supports not (content: "x" / "y") {
a::before {
content: "★";
alt: "Highlighted Text";
}
}
比如上面的示例,在支持的浏览器(比如Chrome浏览器)中Highlighted Text
就会作为★
实体符的可替换文本,屏幕阅读器将会朗读这个可替换文本;对于不支持的浏览器(比如Safari浏览器),它虽不识别content
属性中的第二个值,但可以识别alt
的值,能达到相似的效果。
小结
正如文章所述,在Web页面或应用开发的时候,很多开发者为了尽可能的避免添加额外的DOM结构,在一些场景之下会通过::before
或::after
伪元素来给元素添加子元素(向一个元素中插入元素),这个时候不能缺少content
(哪怕给content
传一个空格符)。除此之外,还能实现一些其他的效果,比如Tooltips效果,自定义计数器,动效,图标等。除了这些,可能还有很多我并没有想到的作用,如果你在这方面有经验,欢迎在下面的评论中与我一起分享。