首页 > 编程笔记

CSS :target伪类用法详解

:target 是 IE9 及以上版本的浏览器全部支持且已经支持很多年的 CSS 伪类,它是一个与 URL 地址中的锚点定位强关联的伪类,其设计的初衷非常好,就是通过锚点标记一些重要的布局元素,当用户访问此链接的时候,可以通过 CSS 代码让这些布局元素主动呈现在用户面前。

这种交互行为的实现无须 JavaScript 代码参与,非常简洁高效。对于这种交互技术至少应该耳熟能详。实际上,对这个伪类有所了解并且在实际项目中有使用经验的开发人员寥寥无几。原因其实很简单,JavaScript 可以实现所有与其类似的需求,没有理由再去学习一个没什么人关注的 CSS 特性。

在多年前 IE 浏览器还是主流浏览器的年代,JavaScript 经常被用来开启弹框广告(直接打开浏览器新窗口的那种广告),用户体验极差,因此很多用户会选择禁用 JavaScript。此时,:target 伪类技术就有了应用场景,因为借助此伪类,就算没有 JavaScript 参与,也能实现非常多的常见交互效果。

现在,由于浏览器的安全升级,JavaScript 很难再去做一些不受欢迎的事情,而现代Web产品越来越复杂,没有 JavaScript 寸步难行,不可能出现用户禁用 JavaScript 的情况,所以,:target 伪类就不怎么受欢迎。

那么问题来了,:target 伪类是否仍需要学习?

根据我多年从业的经验,如果你想在用户体验领域颇有造诣,肯定要学,但是无须把 :target 伪类的分量看得过重,而是可以作为兜底的技术实现策略,主策略仍然使用 JavaScript 完成。如果你只是专注于功能实现,只需了解 :target 伪类的语法和作用,因为 :target 伪类能够完成的事情,JavaScript 也能完成,直接使用 JavaScript 实现就可以了。

接下来正式介绍 :target 伪类。

:target伪类与锚点

假设浏览器地址栏中的地址如下:
https://c.biancheng.net/#cs-anchor
则 #cs-anchor 就是“锚点”,对应术语是哈希(hash 的音译),即 JavaScript 中 location.hash 的返回值。

URL 锚点可以和页面中 id 匹配的元素进行锚定,浏览器的默认行为是触发滚动定位,同时进行 :target 伪类匹配。

举个例子,假设页面有如下 HTML 代码:
<ul>
   <li id="cs-first">第1行,id是cs-first</li>
   <li id="cs-anchor">第2行,id是cs-anchor</li>
   <li id="cs-last">第3行,id是cs-last</li>
</ul>
以及如下 CSS 代码:
li:target {
   font-weight: bold;
   color: skyblue;
}
则呈现的效果如下图所示:


图 1 :target伪类的基本效果

第二行列表的颜色为天蓝色,同时文字加粗显示。这就是 :target 伪类的作用——匹配 URL 锚点对应的元素。

值得一提的是,部分浏览器(如 IE 浏览器和 Firefox 浏览器)下,<a> 元素的 name 属性值等同于锚点值时,也会触发浏览器的滚动定位。

例如:
<a name="cs-anchor">a元素,name是cs-anchor</a>
这种用法是否可以匹配 :target 伪类呢?根据目前的测试,仅 Firefox 浏览器下可以匹配,如果同时有其他id属性值等同于锚点值的元素,例如:
<a name="cs-anchor">a元素,name是cs-anchor</a>
<ul>
   <li id="cs-first">第1行,id是cs-first</li>
   <li id="cs-anchor">第2行,id是cs-anchor</li>
   <li id="cs-last">第3行,id是cs-last</li>
</ul>
则浏览器会优先且唯一匹配 li#cs-anchor 元素,a[name="cs-anchor"] 元素则被忽略。

总而言之,由于兼容性等原因,不推荐使用 <a> 元素加 name 属性值进行锚点匹配。

如果页面有多个元素使用同一个id,则 :target 只匹配第一个元素。例如:
<ul>
   <li id="cs-first">第1行,id是cs-first</li>
   <li id="cs-anchor">第2行,id是cs-anchor</li>
   <li id="cs-last">第3行,id是cs-last</li>
   <li id="cs-anchor">第4行,id同样是cs-anchor</li>
</ul>
则呈现的效果如下图所示,仅第 2 行文字加粗变色,第 4 行文字没有任何变化。


图 2 :target伪类仅匹配第一个元素

然而,IE 浏览器却不走寻常路,第 2 行和第 4 行的 <li> 元素全匹配了,如下图所示。


图 3 IE浏览器下:target伪类匹配全部元素

因此,一定不要使用重复的 id,这既会造成不兼容,也不符合语义。如果想实现:target伪类匹配多个元素,请借助 CSS 选择符实现,例如父子选择符或者兄弟选择符等。

当我们使用 JavaScript 改变 URL 锚点值的时候,也会触发 :target 伪类对元素的匹配。例如,运行如下 JavaScript 代码,:target 伪类就会匹配页面中对应的 #cs-anchor 元素并产生定位效果:
location.hash = 'cs-anchor';
如果匹配锚点的元素是 display:none,则所有浏览器不会触发任何滚动,但是 :target 伪类依然匹配 display:none 元素。例如:
<ul>
   <li id="cs-first">第1行,id是cs-first</li>
   <li id="cs-anchor" hidden>第2行,id是cs-anchor</li>
   <li id="cs-last">第3行,id是cs-last</li>
</ul>
:target + li {
   font-weight: bold;
   color: skyblue;
}
则第 3 行文字将表现为天蓝色同时被加粗,如下图所示。


图 4 :target伪类依然匹配display:none元素

千万不要小看这种行为表现,设置元素 display:none 同时进行 :target 伪类匹配是我所知道的实现诸多交互效果同时确保良好体验的唯一有效手段。

:target伪类交互布局技术简介

:target 不仅可以标记锚点锚定的元素,还可以用来实现很多原本需要 JavaScript 才能实现的效果。

需要注意的是,下面将介绍的展开与收起效果、选项卡效果都不是最佳实践,甚至可以说缺点大于优点,目的只是抛砖引玉,展示一下 :target 伪类的交互能力。:target 伪类真正适用的场景是巨大侧边栏的展开与收起、评论模块的展开与收起这种唯一模块主体同时模块比较重的场景。

1) 展开与收起效果

例如,一篇文章只显示了部分内容,需要点击“阅读更多”才显示剩余内容,HTML 代码如下:
文章内容,文章内容,文章内容,文章内容,文章内容,文章内容,文章内容……
<div id="articleMore" hidden></div>
<a href="#articleMore" class="cs-button" data-open="true">阅读更多</a>
<p class="cs-more-p">更多文章内容,更多文章内容,更多文章内容,更多文章内容。</p>
<a href="##" class="cs-button" data-open="false">收起</a>
这里依次出现了以下 4 个标签元素:
相关 CSS 代码如下:
/* 默认“更多文章内容”和“收起”按钮隐藏 */
.cs-more-p,
[data-open=false] {
   display: none;
}
/* 匹配后“阅读更多”按钮隐藏 */
:target ~ [data-open=true] {
   display: none;
}
/* 匹配后“更多文章内容”和“收起”按钮显示 */
:target ~ .cs-more-p,
:target ~ [data-open=false] {
   display: block;
}
上述 CSS 的实现原理是把锚链元素放在最前面,然后通过兄弟选择符 ~ 来控制对应元素的显隐变化。

传统实现是把锚链元素作为父元素使用,但这样做有一个严重的体验问题:当 display 属性值不是 none 的元素被锚点匹配的时候,会触发浏览器原生的滚动定位行为,而传统实现方法中的父元素 display 的属性值显然不是 none,于是每当点击“阅读更多”按钮,浏览器都会把父元素瞬间滚动至浏览器窗口的顶部,给用户的感觉就是页面突然跳动了一下,带来了很不好的体验。虽然新的 scroll-behavior:smooth 可以优化这种体验,但是由于兼容性问题,并不是特别好的方案。

综合来看,最好的交互方案就是锚链元素 display:none,同时把锚链元素放在需要进行样式控制的 DOM 结构的前面,通过兄弟选择符进行匹配。

我们来看上面例子实现的效果,默认情况下如下图所示。


图 5 展开更多内容的默认效果

点击“阅读更多”按钮后,URL哈希锚点会变成 #articleMore,这时选择器为 #articleMore 元素设置的 :target 伪类样式就会匹配,于是,一些元素的显示状态和隐藏状态就发生了变化,布局效果如下图所示。


图 6 展开更多内容后的显示效果

整个交互效果的实现没有任何 JavaScript 代码的参与,同时这种实现方法有一个巨大的好处,那就是我们可以借助 URL 地址记住当前页面的交互状态。例如在本例中,在展开更多内容后,我们再刷新页面,内容依然保持展开状态。

不过,在实际开发中,对于上面这种小交互没必要记住展开状态。有些场景则不一样,例如移动端开发中经常会有一些重交互的大面积浮层,这个时候通过锚点标记展开状态就非常有用,尤其是分享给其他用户的时候,进入会自动显示此浮层,这就是非常适合使用 :target 伪类的场景。

双管齐下

:target 伪类交互技术显然是不完美的,原因在于,一是只适合简单的交互,一旦交互元素多,则代码会变得非常不美观且复杂;二是它对 DOM 结构有要求,锚链元素需要放在前面(:has伪类大规模普及后这个不足会存在);三是它的布局效果不稳定。

接着上面的例子,由于 URL 地址中的锚点只有一个,因此一旦页面中的其他位置有一个锚点链接,如 href 的属性值是 ###,用户一点击,原本选中的第二个选项卡就会莫名其妙地切换到第一个选项卡,因为锚点变化了。这可能并不是用户所希望的。

因此,在实际开发中,如果对项目要求很高,推荐使用双管齐下的实践策略,具体如下:
这样,用户体验既保持了敏捷,也保持了健壮,这才是理想的用户体验实现,对于比较重要的项目,建议用这种方式实现,对于不太重要的项目,在权衡成本和收益之后,直接用 JavaScript 代码“一把梭”。

推荐阅读

副业交流群 关注微信公众号,加入副业交流群,学习变现经验,交流各种打法。