1.浅谈逻辑选择器 is、where、not、has
在 CSS 选择器家族中,新增这样一类比较新的选择器 -- 逻辑选择器,目前共有 4 名成员:
:is:where:not:has
1.1 is 伪类选择器
:is() CSS伪类函数将选择器列表作为参数,并选择该列表中任意一个选择器可以选择的元素。
在之前,对于多个不同父容器的同个子元素的一些共性样式设置,可能会出现如下 CSS 代码:
header p:hover,
main p:hover,
footer p:hover {
color: red;
cursor: pointer;
}
而如今有了 :is() 伪类,上述代码可以改写成:
:is(header, main, footer) p:hover {
color: red;
cursor: pointer;
}
1.2 支持多层层叠连用
再来看看这种情况,原本的 CSS 代码如下:
<div><i>div i</i></div>
<p><i>p i</i></p>
<div><span>div span</span></div>
<p><span>p span</span></p>
<h1><span>h1 span</span></h1>
<h1><i>h1 i</i></h1>
如果要将上述 HTML 中,<div> 和 <p> 下的 <span> 和 <i> 的 color 设置为 red,正常的 CSS 可能是这样:
div span,
div i,
p span,
p i {
color: red;
}
有了 :is() 后,代码可以简化为:
:is(div, p) :is(span, i) {
color: red;
}
结果如下:
这里,也支持 :is() 的层叠连用。通过 :is(div, p) :is(span, i) 的排列组合,可以组合出上述 4 行的选择器,达到同样的效果。
当然,这个例子比较简单,看不出 :is() 的威力。下面这个例子就比较明显,这么一大段 CSS 选择器代码:
ol ol ul, ol ul ul, ol menu ul, ol dir ul,
ol ol menu, ol ul menu, ol menu menu, ol dir menu,
ol ol dir, ol ul dir, ol menu dir, ol dir dir,
ul ol ul, ul ul ul, ul menu ul, ul dir ul,
ul ol menu, ul ul menu, ul menu menu, ul dir menu,
ul ol dir, ul ul dir, ul menu dir, ul dir dir,
menu ol ul, menu ul ul, menu menu ul, menu dir ul,
menu ol menu, menu ul menu, menu menu menu, menu dir menu,
menu ol dir, menu ul dir, menu menu dir, menu dir dir,
dir ol ul, dir ul ul, dir menu ul, dir dir ul,
dir ol menu, dir ul menu, dir menu menu, dir dir menu,
dir ol dir, dir ul dir, dir menu dir, dir dir dir {
list-style-type: square;
}
可以利用 :is() 优化为:
:is(ol, ul, menu, dir) :is(ol, ul, menu, dir) :is(ul, menu, dir) {
list-style-type: square;
}
1.3 不支持伪元素
有个特例,不能用 :is() 来选取 ::before 和 ::after 两个伪元素。譬如:
注意,仅仅是不支持伪元素,伪类,譬如
:focus、:hover是支持的。
div p::before,
div p::after {
content: "";
//...
}
不能写成:
div p:is(::before, ::after) {
content: "";
//...
}
1.4 :is 选择器的优先级
看这样一种有意思的情况:
<div>
<p class="test-class" id="test-id">where & is test</p>
</div>
<div>
<p class="test-class">where & is test</p>
</div>
div :is(p, #text-id) {
color: blue;
}
按照理解,如果把上述选择器拆分,上述代码可以拆分成:
div p {
color: blue;
}
div #text-id {
color: blue;
}
那么,我们有理由猜想,带有 #text-id 的 <p> 元素由于有了更高优先级的选择器,颜色将会变成 blue,而另外一个 div p 由于优先级不够高的问题,导致第一段文本依旧是 green。
但是,这里,神奇的是,两段文本都变成了 blue:
是由于,:is() 的优先级是由它的选择器列表中优先级最高的选择器决定的。我们不能把它们割裂开来看。
对于 div :is(p, #text-id),is:() 内部有一个 id 选择器,因此,被该条规则匹配中的元素,全部都会应用 div #id 这一级别的选择器优先级。这里非常重要,再强调一下,对于 :is() 选择器的优先级,我们不能把它们割裂开来看,它们是一个整体,优先级取决于选择器列表中优先级最高的选择器。
1.5:where 伪类选择器
了解了 :is 后,我们可以再来看看 :where,它们两个有着非常强的关联性。:where 同样是将选择器列表作为其参数,并选择可以由该列表中的选择器之一选择的任何元素。
还是这个例子:
:where(header, main, footer) p:hover {
color: red;
cursor: pointer;
}
上述的代码使用了 :where,可以近似的看为:
header p:hover,
main p:hover,
footer p:hover {
color: red;
cursor: pointer;
}
这就有意思了,这不是和上面说的 :is 一样了么?
那么它们的区别在什么地方呢?
1.6 :is 和 :where 的区别
首先,从语法上,:is 和 :where 是一模一样的。它们的核心区别点在于 优先级。
来看这样一个例子:
<div>
<p>where & is test</p>
</div>
CSS 代码如下:
:is(div) p {
color: red;
}
:where(div) p {
color: green;
}
正常按我们的理解而言,:is(div) p 和 :where(div) p 都可以转化为 div p,由于 :where(div) p 后定义,所以文字的颜色,应该是 green 绿色,但是,实际的颜色表现为 color: red 红色:
这是因为,:where() 和 :is() 的不同之处在于,:where() 的优先级总是为 0 ,但是 :is() 的优先级是由它的选择器列表中优先级最高的选择器决定的。
上述的例子还不是特别明显,我们再稍微改造下:
<div id="container">
<p>where & is test</p>
</div>
我们给 div 添加上一个 id 属性,改造上述 CSS 代码:
:is(div) p {
color: red;
}
:where(#container) p {
color: green;
}
即便如此,由于 :where(#container) 的优先级为 0,因此文字的颜色,依旧为红色 red。:where() 的优先级总是为 0 这一点在使用的过程中需要牢记。
1.7 :not 伪类选择器
下面我们介绍一下非常有用的 :not 伪类选择器。
:not 伪类选择器用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类(negation pseudo-class)。
1.8 :not 的优先级问题
下面是一些使用 :not 需要注意的问题。
:not、:is、:where 这几个伪类不像其它伪类,它不会增加选择器的优先级。它的优先级即为它参数选择器的优先级。
并且,在 CSS Selectors Level 3,:not() 内只支持单个选择器,而从 CSS Selectors Level 4 开始,:not() 内部支持多个选择器,像是这样:
/* CSS Selectors Level 3,:not 内部如果有多个值需要分开 */
p:not(:first-of-type):not(.special) {
}
/* CSS Selectors Level 4 支持使用逗号分隔*/
p:not(:first-of-type, .special) {
}
与 :is() 类似,:not() 选择器本身不会影响选择器的优先级,它的优先级是由它的选择器列表中优先级最高的选择器决定的。
1.9 :not() 不能嵌套 :not()
禁止套娃。:not 伪类不允许嵌套,这意味着 :not(:not(...)) 是无效的。
1.10 :has 伪类选择器
有逻辑选择器里面最重磅的 :has 出场了。它之所以重要是因为它的诞生,填补了在之前 CSS 选择器中,没有核心意义上真正的父选择器的空缺。
:has 伪类接受一个选择器组作为参数,该参数相对于该元素的 :scope 至少匹配一个元素。
实际看个例子:
<div>
<p>div -- p</p>
</div>
<div>
<p class="g-test-has">div -- p.has</p>
</div>
<div>
<p>div -- p</p>
</div>
div:has(.g-test-has) {
border: 1px solid #000;
}
1.11 :has() 父选择器 -- 嵌套结构的父元素选择
div:has(>h2>span) {
margin-left: 24px;
border: 1px solid #000;
}
这里,要求准确选择 div 下直接子元素是 h2,且 h2 下直接子元素有 span 的 div 元素。注意,选择的最上层使用 :has() 的父元素 div。结果如下:
1.12 :has() 父选择器 -- 同级结构的兄元素选择
还有一种情况,在之前也比较难处理,同级结构的兄元素选择。
看这个 DEMO:
<div class="has-test">div + p</div>
<p>p</p>
<div class="has-test">div + h1</div>
<h1>h1</h1>
<div class="has-test">div + h2</div>
<h2>h2</h2>
<div class="has-test">div + ul</div>
<ul>ul</ul>
我们想找到兄弟层级关系中,后面接了 <h2> 元素的 .has-test 元素,可以这样写:
.has-test:has(+ h2) {
margin-left: 24px;
border: 1px solid #000;
}
效果如下:
1.13 :has() 兼容性,给时间一点时间
比较可惜的是,:has() 在最近的 Selectors Level 4 规范中被确定,目前的兼容性还比较惨淡,截止至 2022-05-04,Safari 和 最新版的 Chrome(V101,可通过开启 Experimental Web Platform features 体验)
2.animation-fill-mode 控制元素在各个阶段的状态
首先,动画只运行一次,未运行前处于第一帧,运行完后处于最后一帧。
这个刚好利用 CSS 动画的 animation-fill-mode: both 即可。
-
animation-fill-mode: backwards:可以让元素在动画开始之前的样式为动画运行时的第一帧,动画结束后的样式则恢复为 CSS 规则设定的样式 -
animation-fill-mode: forwards:元素在动画开始之前的样式为 CSS 规则设定的样式,而动画结束后的样式则表现为由执行期间遇到的最后一个关键帧计算值(也就是停在最后一帧) -
animation-fill-mode: both兼顾了上面两种模式的特点,可以使得动画开始前的样式为动画运行时的第一帧,动画结束后停在最后一帧。
3.反向利用 animation-play-state 实现 hover 触发动画行进
而动画通过 hover 驱动,只有用户 hover 元素的时候,动画才进行这一点,利用 animation-play-state 即可。
我们都知道,正常情况下,动画应该是运行状态,那如果我们将动画的默认状态设置为暂停,只有当鼠标点击或者 hover 的时候,才设置其 animation-play-state: running,这样就可以利用 hover 控制动画的行进!
基于上述两点,我们来实现一个有意思的打字动画,做到动画只触发单次,并且只有 hover 的时候动画会运行。
有意思,完美的实现了上面说的要求 -- 动画通过 hover 驱动,只有用户 hover 元素的时候,动画才进行。 当然,这里还运用了几个小技巧,一并解释下:
- 打字动画运用了逐帧动画,而不是补间动画,主要利用了 CSS 动画的
step-timing-function步骤缓动函数,也就是代码中的steps(15, end) ch是 CSS 当中的一个相对单位,这一单位代表元素所用字体 font 中 “0” 这一字形的宽度font-family: monospace表示等宽字体,每个字符占据的宽度是一样,因为我们使用了26ch来充当<p>元素的宽度,而Hover Me - You are a pig这一段文字算上空格刚好 26 个字符,26ch刚好表示这一段文本的长度- 一开始展示的文本
Hover me -算上空格是11ch宽度,而最后整个文本展示完需要26ch的宽度,中间需要经过 15 步的逐帧动画,这里的元素刚好和代码中的一一对应上
借助上面 4 步及搭配我们上文介绍的 animation-fill-mode: both、animation-play-state: paused 的应用,我们就完美的实现了这样一个非常有意思的打字动画。
理解steps steps 函数指定了一个阶跃函数 第一个参数指定了时间函数中的间隔数量(必须是正整数) 第二个参数可选,接受 start 和 end 两个值,指定在每个间隔的起点或是终点发生阶跃变化,默认为 end。 step-start等同于steps(1,start),动画分成1步,动画执行时为开始左侧端点的部分为开始; step-end等同于steps(1,end):动画分成一步,动画执行时以结尾端点为开始,默认值为end。
总结: steps函数,它可以传入两个参数,第一个是一个大于0的整数,他是将间隔动画等分成指定数目的小间隔动画,然后根据第二个参数来决定显示效果。
第二个参数设置后其实和step-start,step-end同义,在分成的小间隔动画中判断显示效果。可以看出:steps(1, start) 等于step-start,steps(1,end)等于step-end
最核心的一点就是:timing-function 作用于每两个关键帧之间,而不是整个动画。
4. 何为 @scroll-timeline 滚动时间线?
什么是 @scroll-timeline 滚动时间线呢?
@scroll-timeline 能够设定一个动画的开始和结束由滚动容器内的滚动进度决定,而不是由时间决定。
意思是,我们可以定义一个动画效果,该动画的开始和结束可以通过容器的滚动来进行控制。
@scroll-timeline 语法介绍
接下来,我们先缓一缓,简单看一看 @scroll-timeline 的语法。
使用 @scroll-timeline,最核心的就是需要定义一个 @scroll-timeline 规则:
@scroll-timeline moveTimeline {
source: selector("#g-content");
orientation: vertical;
scroll-offsets: 0px, 500px;
}
其中:
-
source:绑定触发滚动动画的滚动容器
source: auto:绑定到Document,也就是全局 Windows 对象source: selector("id-selector"),通过selector(),内置一个#id选择器,选取一个可滚动容器source: none:不指的滚动容器
-
orientation:设定滚动时间线的方向
orientation: auto:默认为 vertical,也就是竖直方向的滚动orientation: vertical:竖直方向的滚动orientation: horizontal:水平方向的滚动orientation: block:不太常用,使用沿块轴的滚动位置,符合书写模式和方向性orientation: inline:不太常用,使用沿内联轴的滚动位置,符合书写模式和方向性
-
scroll-offsets:滚动时间线的核心,设定在滚动的什么阶段,触发动画,可通过三种方式之一进行设置:
scroll-offsets: none这意味着没有 scroll-offset 指定。- 由逗号分隔的值列表确定。每个值都映射到animation-duration。例如,如果 ananimation-duration 设置为 2s 且滚动偏移量为 0px, 30px, 100px,则在 1s 时,滚动偏移量将为 30px。
- 第三种确定滚动偏移量的方法是使用元素偏移量。这意味着可以指定页面内的元素,其位置决定了滚动时间线以及要使用这些元素的哪个边缘。指定元素是使用 selector() 函数完成的,该函数接收元素的 id。边缘由关键字 start 或确定 end。可选的阈值的 0–1 可用于表示元素滚动中预期可见的百分比。
scroll-offsets 的理解会比较困难,我们稍后详述。
在设定了一个 @scroll-timeline 之后,我们只需要将它和动画绑定起来即可,通过 animation-timeline:
@scroll-timeline 虽好,目前仍处于实验室特性时间,Chrome 从 85 版本开始支持,但是默认是关闭的。
兼容性如下(2022-03-07):
在最新的 chrome、Edge、Opera 可以通过浏览器配置开启该特性,Chrome 下开启该特性需要:
- 浏览器 URL 框输入
chrome://flags - 开启
#enable-experimental-web-platform-features
通过 @supports (animation-timeline: works) {} 可以判断浏览器是否支持 @scroll-timeline。
5. CSS Overview Panel
5.1 什么是 CSS Overview Panel
CSS Overview Panel 直译过来就是 CSS 概览面板,它是从 Chrome 87 开始支持的一项实验室功能。
属于控制台 DevTool 下的一个 TAB:
默认这个实验室功能是关闭的,也就是我们打开控制台是看不到的,那么我们如何打开呢?两种方式:
- 从 DevTools 的 Setting 下的 Experiment 菜单中,启用 CSS Overview
- 打开 DevTools,通过
Cmd + Shift + P呼出 Command Menu,输入Show CSS Overview
5.2 如何更好的利用 CSS Overview?
那么,我们应该在什么时候开始使用它或者它能够帮助我们做些什么呢?我个人认为一些比较核心的点:
5.3 更准确(高保真)的还原设计稿,辅助设计走查环节
在对设计稿还原有更高要求的页面上,在设计走查环节,非常适合利用这个面板去查看页面的颜色使用、字体使用是否合乎规范。
因为通常设计师会制定一系列规范,譬如什么地方用什么颜色/字体。但是由于前端在重构设计稿的过程中,因为某些原因(譬如取色器不够精确,想当然的认为某些色值是黑色或者白色)。
那么利用 Color 和 Font 模块,可以明确的找到不在规范内的颜色或字体,进行修改。
5.4 更好的精简我们的 CSS 代码
这一点非常好理解,利用 Unused declarations(未使用的样式规则)模块,我们可以很好的找到未被使用的 CSS 代码,在确定后剔除掉。
5.5 辅助进行网站的可访问性提升
6. sroll-snap
6.1 sroll-snap-type
首先看看 sroll-snap-type 可能算得上是新的滚动规范里面最核心的一个属性样式。
scroll-snap-type:属性定义在滚动容器中的一个临时点(snap point)如何被严格的执行。 光看定义有点难理解,简单而言,这个属性规定了一个容器是否对内部滚动动作进行捕捉,并且规定了如何去处理滚动结束状态。
语法
{
scroll-snap-type: none | [ x | y | block | inline | both ] [ mandatory | proximity ]?
}
上面 scroll-snap-type: x mandatory 中,x 表示捕捉 x 轴方向上的滚动,。
- mandatory: 通常在 CSS 代码中我们都会使用这个,mandatory 的英文意思是强制性的,表示滚动结束后,滚动停止点一定会强制停在我们指定的地方
- proximity: 英文意思是接近、临近、大约,在这个属性中的意思是滚动结束后,滚动停止点可能就是滚动停止的地方,也可能会再进行额外移动,停在我们指定的地方
6.2 scroll-snap-align
使用 scroll-snap-align 可以简单的控制将要聚焦的当前滚动子元素在滚动方向上相对于父容器的对齐方式。
scroll-snap-align 很好用,可以控制滚动子元素与父容器的对齐方式。然而可选的值只有三个,有的时候我们希望进行一些更精细的控制时,可以使用 scroll-margin 或者 scroll-padding
其中:
- scroll-padding 是作用于滚动父容器,类似于盒子的 padding
- scroll-margin 是作用于滚动子元素,每个子元素的 scroll-margin 可以设置为不一样的值,类似于盒子的 margin
兼容问题:
7. position:sticky[ 实现粘性布局]
-
须指定 top, right, bottom 或 left 四个阈值其中之一(且达到设定的阈值),才可使粘性定位生效。否则其行为与相对定位相同;
- 并且 top 和 bottom 同时设置时,top 生效的优先级高,left 和 right 同时设置时,left 的优先级高
-
设定为
position: sticky的元素的任意父节点的 overflow 属性必须是 visible,否则position:sticky不会生效;- 如果 position: sticky 元素的任意父节点定位设置为 position: overflow,则父容器无法进行滚动,所以 position:sticky 元素也不会有滚动然后固定的情况
-
在满足上述情况下,设定了
position: sticky的元素的父容器的高度必须大于当前元素,否则也会失效。(当然,此时,sticky吸附的基准元素就会变成父元素)