CSS:深入content

如果你的灵魂进入了另一个人的身体,那么他是他还是你呢?content属性是一个元素的心,当你偷走了它的心,它就不再像它了,而是像一个——替换元素。

替换元素

是否为替换元素是CSS中的另一种分类。元素根据其外在盒子被分为块状元素内联元素,而根据其内容是否替换分为替换元素非替换元素

特性

在我看来,替换元素分为两种:

一种是内容可替换的元素,即img、object、video、iframe和表单元素input、textarea、select等等都是内容可替换的,这里的内容可替换是指它们的内容可以通过改变标签的属性值(如src或value)就可以改变,即:可通过属性替换其内容。

另一种是内容被替换的元素,即原本不是替换元素的普通元素,通过content内容生成技术为其生成了内容,使其的表现几乎完全相同于替换元素,那么它们就是内容被替换的元素。

那么替换元素的特性或者说表现都有哪些?主要有:

  • 其内容的外观不受页面上CSS的影响。如input单选框或复选框,它们的一些样式是被固定的,你无法通过写CSS来改变它们的这些样式如背景色、内补间等。
  • 拥有自己的默认尺寸。很多替换元素如video、iframe、canvas等在没有明确规定尺寸时,尺寸表现为300×150px,也有img这样的默认尺寸为0,也有像input这样的有其默认的其他尺寸。
  • 还有一些与众不同的CSS表现值。比如vertical-align默认为基线,而替换元素则默认元素下边缘。

那么问题来了,替换元素与非替换元素是独立于块状元素和内联元素的分类体系,那么替换元素是块状元素还是内联元素?实际上,他们中有块状元素也有内联元素,甚至在不同浏览器中也不一样:

尺寸

尺寸的种类

替换元素的尺寸由内而外分为以下三种:

  • 固有尺寸:即替换的内容本身的尺寸,如一张图片的尺寸。
  • HTML尺寸:通过HTML原生属性指定的尺寸,如图片的width、height属性,input的size属性,textarea的cols和rows属性。
  • CSS尺寸:指使用CSS设定的尺寸,包括了widthheightmax/min-widthmax/min-height等。

这些尺寸的优先级关系是CSS尺寸>HTML尺寸>固有尺寸,即当没有指定HTML尺寸和CSS尺寸的时候,替换元素表现为固有尺寸,当指定了HTML尺寸则表现为HTML尺寸,当指定了CSS尺寸就表现为CSS尺寸。

这里要注意HTML尺寸要和行内样式、外部样式的优先级进行区别,这里的HTML尺寸设定的不是行内样式!因此优先级弱于CSS样式。对于CSS样式,依然是行内样式优先级大于外部样式表。

例外的图片

上面说到,对于video,没有设定尺寸的默认尺寸是300×150px,在认知中似乎图片应该和视频表现一样才对,而图片的默认尺寸却是0,原因是video我们一般不会让它和文字混在一起排版,而图文混排的情况太多了,甚至还会有文字中掺杂着小图标、小表情的情况出现,这时如果图片加载出了问题,或者意外遗落了img标签,未为它设置样式和加载源,那么它在文字中间突兀地出现了一大片300×150px的空白区域,岂不是很离谱?因此img默认尺寸就是0,遗落的img标签就让它看起来消失而不影响原本的文字排版。

可是还有个意外,就是IE浏览器,总是表现得和其他主流浏览器与众不同,它的img标签什么都没有时,会使用一个28×30px的占位图标。

从图片懒加载说起

图片懒加载其实就是如果一个页面上有很多图片,并不在页面打开时一起加载,而是懒懒地等着用户就要滚动到了它的位置,不得不加载了再去请求图片进行加载,像极了每次任务不到DDL就不会去做的你。

图片懒加载是一种网页性能优化的方式,它能极大的提升用户体验,提高打开页面的速度,并且如果用户并没有往下翻页,下面的图片不会被加载,节省了用户的流量。图片懒加载就需要使用一个没有src属性的img标签来占位。

占位img

我们直接用一个裸的img标签来占位即可,同时设置:

1
2
img { visibility: hidden; }
img[src] { visibility: visible; }

之后需要加载的时候使用JS为img标签设置src属性即可。

之所以使用裸的、没有src的标签,是因为很多浏览器只要有src属性就会产生请求,完全不让它产生请求就不可以有src属性,这是最高效的实现方式。

但是这里问题出现了,我们希望元素按照一定宽高占位,没有内容的img的默认尺寸为0,使用CSS尺寸来为其设定尺寸,这在Chrome和IE浏览器下都没有问题,却在Firefox浏览器下失效了。此时要想解决这个兼容性问题,只需要:

1
img { display: inline-block; }

对于在这种图片懒加载的场景下,为防止占位img标签宽高失效,可以在这个页面上进行这样的全局重置。

固有尺寸不可改变

由于图片作为替换元素有上述三种尺寸,那么很多人就会认为好像给img标签设置宽高就改变了图片的固有尺寸一样,实际上不是的,图片的固有尺寸是完全取决于图片文件的本来尺寸的,使用HTML和CSS是无法改变的,但是之所以改变img标签的宽高就能够改变图片的尺寸,是因为图片在标签中的填充模式为fill,即充满标签。

在CSS3中有一个新的属性可以用来修改替换内容的适配方式,即object-fit,它默认即为fill,它支持的值有:

  • fill:默认,不保证保持原有的比例,内容拉伸填充整个内容容器。
  • contain:保持原有尺寸比例,内容被缩放以尽可能利用HTML标签的尺寸但又不会超出。
  • cover:保持原有尺寸比例,但是会尽可能覆盖标签的空间不留下空隙,而内容可能会被裁剪。
  • none:保留原有元素内容的固有尺寸,内容尺寸将不受控制。
  • scale-down:保持原有尺寸比例。内容的尺寸与 nonecontain 中的一个相同,取决于它们两个之间谁得到的对象尺寸会更小一些。

那么这么一看,你可能想到了那个东西。对,就是背景图片。

背景图片的填充模式也是和上述类似的,而背景图片默认的填充模式就是不拉伸,以固有尺寸显示图片,但是会重复背景图片以填满标签。

实际上,还有一种情况就是在::before::after伪元素中使用content生成的图片内容,这时候图片就会以固定尺寸来显示,并且你还无法改变它的尺寸:

替换元素的界限

实际上,如果非要纠结替换元素和非替换元素之间的界限,无异于纠结替换元素的准确定义,可在不同的浏览器中,替换元素的定义和表现总会有这样那样的差别。因此这部分内容我强烈建议你只用来了解,不必深究,因为这些东西包含了一些反模式的“奇技淫巧”,应当避免使用的(比如把img标签当span来用)。之所以还是要写这部分内容,是因为稍微了解它们也许在你的代码出现莫名其妙的bug时,能够从中找到一些原因。

典型的替换元素

典型的替换元素自然是公认的像img标签这样的可替换元素,它们生来就是替换元素……吗?这里我们深究的话,如果你正常使用一张图片,它确实是替换元素,可在某些情况下,它也可以是一个无异于span标签的普通元素。这种情况就是前面说的:没有src属性时。

在Firefox浏览器中,如果你使用一个裸的、不带src属性的img标签,它的表现完全就像一个裸的span标签一样,表现上没有丝毫不同。这时你如果再给它设置成display: block;,那么它的表现和一个div元素又没有任何不同了,就好像它就是一个普普通通的非替换元素一样。

而在Chrome浏览器中(截至写本文时),裸的img标签会表现为像span一样的内联标签,而设置了display: block;反而会使它回归替换元素的本性,表现为0×0的尺寸,而无法表现得像div标签一样。

还有就是,某些浏览器下没有src但是有alt属性的img标签会显示出一个裂开的图片小图标并显示出alt指定的信息,要想在懒加载没有添加src属性时隐藏掉它,可以这样:

1
img:not([src]) { visibility: hidden; }

非典型的替换元素

非典型的替换元素就是你认为它生来不是替换元素,但我们使用CSS的content属性替换了它的内容,替换了之后我们也称它是替换元素了。

为什么这样说呢?实际上你可以尝试给一个div使用content属性设置一张图片,发现它的表现和一个设置了src属性的img标签没什么两样。甚至上面我们不是说裸的img标签就表现得和一个普普通通的span标签没什么两样了吗?但是我们通过content给它生成一个图片内容,发现它竟然表现得又和设置了src的img标签一样了!

啊不好意思,实际上说一样了也不完全正确,因为content属性生成的内容始终有一个特性,即无法选中、复制等操作,就好像生成的东西只是一个虚拟的像一样,无法对这些内容进行操作。就好像之前我们也尝试过,在::before::after伪元素里使用content生成的文字没法选中,也没法获取,它们不是页面内容的一部分,而是样式的一部分。

因此有了这样的一个例子:

HTML:

1
<img src="1.png">

CSS:

1
img { content: url(2.png); }

效果如下:

你看它是一个完全没问题的图片对吗,你可以尝试把它保存到本地(鼠标右键另存为)试一试,看看保存下来的图片。

是不是很有意思?你保存下来的图片竟然和显示的不一样?

这个例子加工一下就成了这样:

1
img:hover { content: url(2.png); }

效果如下:

当鼠标经过图片时,图片变成了另一张图片,从笑脸变成了笑哭,原理就是这个img标签原本的src指向的是笑脸,而鼠标经过时使用content替换了其中的内容为一张笑哭的表情,你尝试将这张图片保存下来,你会发现笑哭的图片无论如何都无法保存下来,因为它不属于页面内容,只是一个生成的表面的像,真正保存下来的只能是img标签的src属性指向的图片。

content内容生成技术

使用CSS的content属性生成标签内容,我们称其为content内容生成技术,我们可以通过它做到许多非(huā)常(lǐ)有(hú)用(shào)的事情。

字符内容生成

字符内容的生成就很简单了,在::before::after伪元素中我们经常用到(因为这两个伪元素中必须要有content属性,没有的话元素就不会显示,哪怕我们常常将它设置为空字符串,仅仅是为了让伪元素显示出来)。

此处不再赘述字符生成。

清除浮动

清除浮动我们往往需要多余的标签来设置clear属性,而使用伪元素加上空的content可以生成用来清除浮动的伪元素:

1
2
3
4
5
.clear::after {
content: '';
display: table; /* 或block */
clear: both;
}

辅助对齐

同样是空的content来生成的伪元素,还能够辅助元素对齐。

有如下代码:

HTML:

1
2
3
4
5
<div id="box" class="box"><i class="bar"></i>
<i class="bar"></i>
<i class="bar"></i>
<i class="bar"></i>
</div>

CSS:

1
2
3
4
5
6
7
8
9
10
11
12
.box {
width: 180px;
height: 180px;
border-bottom: 2px dashed black;
text-align: justify;
}
.bar {
display: inline-block;
width: 20px;
height: 120px;
background-color: blue;
}

效果如下:

可以看到四个柱子并排放置,而我们希望绘制出柱状图一样的对齐方式,即四根柱子都靠着容器底部对齐,而不是现在这样靠着容器顶部;还要让它们两端对齐,平均分布,而不是像现在这样靠在左边。那么只需要生成这样的::before::after伪元素:

1
2
3
4
5
6
7
8
9
10
.box:before {
content: "";
display: inline-block;
height: 100%;
}
.box:after {
content: "";
display: inline-block;
width: 100%;
}

即可达到效果:

我们可以看到是两个伪元素分别撑起了宽和高,但具体的原理我们先按下不表,这不属于本文内容。

图片生成

内容与显示

使用content是可以给普通元素生成图片的,用图片来替换元素原本的内容,那么问题来了,像这样的代码:

HTML:

1
<div>大森的博客</div>

CSS:

1
2
3
4
div {
content: url(3.png);
width: 200px;
}

效果如下:

大森的博客

那么问题来了:替换了div中的内容之后,div中原本的内容哪儿去了?

实际上这种方式是兼顾了表现效果和SEO的。SEO为搜索引擎优化,也就是搜索引擎在收录你的网站时,会抓取你的页面上的内容作为网站概要,而抓取的内容主要就是文字。

如果这里完全使用img来代替div标签,将导致搜索引擎抓取不到对应的文字信息,也就无法得到网页的概要;对于屏幕阅读器来说,页面上使用图片来替代文字的内容将无法被阅读,这可能给使用它的人造成困扰。

如果使用内容生成技术,像这样使用图片代替文字的情况下,原本的h1标签内的文字信息依然能被抓取到,即内容还在HTML中,而CSS提供了单独的显示效果,这样能够同时兼顾显示效果和页面内容不丢失。

生成内联图片

content通过url()功能符生成图片,而这样并非只能生成外部图片,它还可以生成base64图片,这样的图片是内联在CSS文件中的,因此不会从外部加载,防止了图片从无到有加载过程中的页面抖动。

如下面的CSS即为使用base64地址生成一张图片:

1
2
3
div {
content: url(data:image/png;base64,iVBORw0KGgoAAAANS...);
}

(代码中的地址内容过长,故省略。)

生成的图片如下:

但是注意,这种方式生成图片往往比直接加载原图片占用更多的网络资源。

开启闭合符号生成

可以像这样指定开始/结束符号:

1
2
3
4
5
6
.ask {
quotes: "问:“" "”";
}
.answer {
quotes: "答:“" "”";
}

然后使用open-quoteclose-quote关键字使content生成开始和结束符号。

1
2
3
4
5
6
7
8
.ask::before,
.answer::before {
content: open-quote;
}
.ask::after,
.answer::after {
content: close-quote;
}

属性值内容生成

可以在content属性中使用attr()功能符,从属性中获取值并生成内容。如可以为img生成描述信息:

1
2
3
img::after {
content: attr(alt);
}

content计数器

content计数器功能非常强大、实用,是content部分的重中之重。在很多情况下它具有不可替代性,甚至可以实现连JavaScript都不好实现的效果。

counter-reset属性

该属性的作用是重置或初始化一个计数器,你需要指定计数器的名称以及初始从哪个值开始。如果没有指定初始值,它将从0开始。初始值最好不要设置为负数和小数,因为部分浏览器可能不兼容,不兼容的话依然是默认值0。如下面的属性为.counter元素初始化了一个计数器counter1:

1
2
3
.counter {
counter-reset: counter1;
}

而下面代码是同时指定了两个计数器,并分别从0和1开始:

1
2
3
.counter {
counter-reset: counter1 0 counter2 1;
}

counter()功能符

该功能符引用一个计数器,使用计数器名称作为参数,这样我们就可以使用上面定义的计数器的值了:

HTML:

1
<div class="counter"> / </div>

CSS:

1
2
3
4
5
6
7
8
9
.counter {
counter-reset: counter1 0 counter2 1;
}
.counter::before {
content: counter(counter1);
}
.counter::after {
content: counter(counter2);
}

效果如下:

/

实际上counter()功能符还支持第二个参数,即list-style-type属性所支持的参数:

1
2
3
4
5
6
list-style-type: disc | circle | square | decimal 
| lower-roman | upper-roman | lower-alpha
| upper-alpha | none | armenian | cjk-ideographic
| georgian | lower-greek | hebrew | hiragana
| hiragana-iroha | katakana | katakana-iroha
| lower-latin | upper-latin

那么我们可以这样使用大写罗马数字来显示计数器内容:

1
2
3
4
5
6
.counter::before {
content: counter(counter1,upper-roman);
}
.counter::after {
content: counter(counter2,upper-roman);
}

counter-increment属性

我们已经知道了该如何初始化和显示一个计数器,那么最重要的就是如何让这个计数器开始计数?即如何递增。

此时应当使用counter-increment属性来实现,该属性需要提供计数器名称作为属性值,并可提供第二个可选的递增步长,不指定递增步长的话默认为1。

在渲染页面元素时,每渲染一次counter-increment属性,都会增加对应的值,如下面的例子:

HTML:

1
2
3
4
5
<div class="counter">
<div class="count">No. </div>
<div class="count">No. </div>
<div class="count">No. </div>
</div>

CSS:

1
2
3
4
5
6
7
.counter {
counter-reset: cnt 0;
}
.count::after {
content: counter(cnt);
counter-increment: cnt;
}

效果如下:

No.
No.
No.

需要注意的一些问题:

  • counter()功能符只能用在伪元素中来生成内容,普通元素无效。
  • counter-increment属性也可以一次性递增多个计数器的值,和初始化计数器的方式一样。
  • 递增的值可以为负数,这样就实现了递减。
  • counter()功能符生成的值是静态的,之后计数器的值改变了并不会影响已生成的值。

counters()功能符

通过counters()功能符可以实现嵌套计数,实现二级标题。

该功能符需要两个参数,第一个为计数器名称,第二个为多级标题的连接符,如"1.1"的连接符即为".",而"1-1"的连接符为"-"

此时你的小小的眼睛一定充满了大大的疑惑:既然是多级计数器,为什么只有一个计数器名称呢,难道不应该是计数器1对应着第一级标题,然后计数器2对应着第二级标题吗?

这里就要感叹设计者的高明了:如果如你所想,那么有几层标题就得需要几个计数器来单独计数,也不是不行,但是不够灵活,比如说内容是动态加载的,而你实现并不知道有几层标题呢?

所以这里就要说到counter-reset的本质了——初始化或重置——重置的即是嵌套的计数器,有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="reset">
<div class="count"> 第一章</div>
<div class="reset">
<div class="count"> 第一章 第一节</div>
<div class="count"> 第一章 第二节</div>
</div>
<div class="count"> 第二章</div>
<div class="reset">
<div class="count"> 第二章 第一节</div>
<div class="count"> 第二章 第二节</div>
</div>
<div class="count"> 第三章</div>
<div class="reset">
<div class="count"> 第三章 第一节</div>
<div class="count"> 第三章 第二节</div>
</div>
</div>

对于这样的结构,我们可以看出,每当新开一级目录,都进行reset,即CSS是这样的:

1
2
3
4
5
6
7
.reset {
counter-reset: cnt 0;
}
.count::before {
content: counter(cnt);
counter-increment: cnt;
}

效果如下:

第一章
第一章 第一节
第一章 第二节
第二章
第二章 第一节
第二章 第二节
第三章
第三章 第一节
第三章 第二节

这是我使用的是counter()功能符而非counters()功能符,可以看到显示结果是按照我们已知的规则进行的:即按照页面渲染顺序,遇到reset就重置,遇到increment就递增。如果我改用counters()功能符呢?

我们把每一个reset都塞进它的上一级的count里面,即把第一章的两个章节构成的reset塞进第一章的count里面,然后使用counters()功能符:

HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="reset">
<div class="count"> 第一章
<div class="reset">
<div class="count"> 第一章 第一节</div>
<div class="count"> 第一章 第二节</div>
</div>
</div>
<div class="count"> 第二章
<div class="reset">
<div class="count"> 第二章 第一节</div>
<div class="count"> 第二章 第二节</div>
</div>
</div>
<div class="count"> 第三章
<div class="reset">
<div class="count"> 第三章 第一节</div>
<div class="count"> 第三章 第二节</div>
</div>
</div>
</div>

CSS:

1
2
3
4
5
6
7
8
.reset {
counter-reset: cnt 0;
padding-left: 20px;
}
.count::before {
content: counters(cnt, "-");
counter-increment: cnt;
}

效果如下:

第一章
第一章 第一节
第一章 第二节
第二章
第二章 第一节
第二章 第二节
第三章
第三章 第一节
第三章 第二节

可以看出一个reset就是一个层级,我给每个层级还加了padding-left便于观察,这时就可以看到完美的二级标题出现了。

这样只要按照规则去嵌套HTML标签,简单的CSS就可以实现出任意层级的多级标题,完美!

此外counters()功能符也是支持第三个参数用来指定数字的显示样式的,和counter()功能符的第二个参数一样。

总结

如果你只需要一级计数器,那么你的用来显示content: counter(cnt);的标签应当是互相无嵌套的,这时每次reset都会重置计数器,每次increment都会递增计数器,计数的顺序按照渲染顺序。

如果你需要多级计数器,那么对于每个显示content: counters(cnt, "-");的count标签,它的下一级的所有count标签都应当有一个共同的reset父容器,并且这个reset容器应当作为它的直接子标签,下一级的所有count标签又要作为它们reset容器的直接子标签,如此嵌套下去即可。总之reset容器和count标签必须为父子关系,不能以兄弟关系出现,否则必乱套。

混合内容生成

content是可以生成多个内容的,中间以空格分隔即可。如:

1
2
3
4
5
6
7
8
9
a:after {
content: "(" attr(href) ")";
}
q:before {
content: open-quote url(1.jpg);
}
.counter:before {
content: counters(wangxiaoer, '-') '. ';
}



* 你好,我是大森。如果文章内容帮到了你,你可通过下方付款二维码支持作者 *