头图

浅谈 :focus 伪类选择器和聚焦后 outline 边框的设置问题

浏览器一般会自带 :focus{outline:1px solid blue;} 这样样式设置,当元素被聚焦时会显示出外边框,这样用户就能知道当前浏览器的焦点位置,但外边框的样式往往会显得多余又难看,所以开发人员时常会选择将外边框去掉,也就是设置 :focus{outline:0;}

但是将聚焦后的外边框去掉这种做法,实际上是不可取的,因为这将导致当用户使用键盘的 Tab 按键进行焦点定位时,根本不知道自己聚焦到什么地方去,这种情况下用户体验就会很差了,关于这一点,可以参考《绝不要删除样式的外边框》《为你的网站设置有用和可用的焦点指示器样式》这两篇文章(备注:两篇文章都是英文的)。

而我们所面临的问题,其实简单来讲就是,我们不希望在点击聚焦时出现外边框,因为样式很难看,但是又希望在使用键盘聚焦时显示出外边框,因为不这么做会影响到用户体验,前端领域一直有在探索这个问题要如何解决,在以前只能是使用 JS 进行聚焦的判断处理,而现在我们可以使用 CSS4 新增的 :focus-visible 伪类选择器来处理,接下来 XJ 会大致讲一下这两种方案的特点以及它们的局限性。


:focus-visible 选择器

:focus-visible 是 CSS4 新增的一个伪类选择器,它与 :focus 伪类选择器十分相似,都是在标签节点被聚焦的时候生效,唯一的区别是 :focus 伪类选择器不会区分导致聚焦的操作,只要聚焦了就会生效,而 :focus-visible 伪类选择器只会在聚焦由键盘导致时才生效,但可编辑元素如 <input type="text" /> 除外,这类元素不管聚焦是什么操作导致的都会生效,下面是一个简单的例子:

<style>
    /* 鼠标点击导致的聚焦,边框将是红色的 */
    /* Tab 按键导致的聚焦,边框将是绿色的 */
    :focus{outline:2px solid red;}
    :focus-visible{outline:2px solid green;}
</style>

<p>
    普通标签通过点击聚焦,边框是红色,<br />
    通过键盘的 Tab 键聚焦,边框是绿色。<br />
    <a href="javascript:void(0)">anchor</a>
    <button type="button">button</button>
    <span tabIndex="0">span[tabIndex="0"]</span>
</p>

<p>
    可输入的总会匹配 :focus-visible 选择器,<br />
    不管聚焦由什么操作导致,边框都是绿色。<br />
    <span contentEditable="true">span[contentEditable="true"]</span><br />
    <input type="text" placeholder="text" /><br />
    <textarea ></textarea><br />
</p>

↓ View & Code ↑

看着好像不错?然而 XJ 认为这个 :focus-visible 伪类选择器并没有想象中那么好用,首先是它对可输入的元素采取了特殊处理,这可能并不总是符合我们的需求,其次是兼容性问题,需要 Firefox85+ 和 Chrome86+ 才能支持,IE 和 Safari 你就别奢望了,更详细的兼容信息可查看 MDNcanIuse,如果我们想要所有浏览器都支持,就得借助 JS 绑定事件来处理,而这则是下一章将会讲到的内容。

这里再补充一个信息,早在 Firefox4.0 的时代,Firefox 就有个差不多概念的伪类选择器既 :-moz-focusring,但是这个选择器最终并没有变成标准,并且它的功能和现在的 :focus-visible 伪类选择器有蛮大的差别,有些人可能会用这个选择器去做 Firefox 低版本的兼容写法,但实际上并没有多大的用处,这个选择器现在也已经被废弃,可以不用理会了,所以在上面的 Demo 中 XJ 也没用到它。


使用 JS 来解决这个问题

使用 JS 来解决这个问题以实现兼容,最简单的做法,就是绑定鼠标事件和键盘事件,在触发了键盘事件的时候就在 <html> 上添加一个类名如 isKbd,让 .isKbd :focus{} 样式规则生效,在触发了鼠标事件的时候就移除 <html> 上的 isKbd 类名让 .isKbd :focus{} 样式规则不生效,下面是一个简单的例子,当聚焦是点击导致的就没有外边框,当聚焦是按了键盘导致的就会有红色外边框:

<style>
    /* 先将 :focus 默认的所有的外边框都清理掉 */
    /* 当聚焦是由于按了键盘导致时显示红色边框 */
    :focus{outline:0;}
    .isKbd :focus{outline:2px solid red;}
</style>

<p>
    聚焦由点击操作触发时没有外边框,<br />
    聚焦由键盘触发时显示红色外边框。<br />
</p>
<p>
    <button type="button">button</button><br />
    <a href="javascript:void(0)">anchor</a><br />
    <input type="text" placeholder="text" /><br />
    <span contentEditable="true">span[contentEditable="true"]</span><br />
</p>

<script>
(function(){

// 获取 html 节点,创建用于判断操作状态的布尔值变量
var html = document.documentElement;
var isKbd = false;

// 当键盘被按下时,html 标签节点将被添加 isKbd 类名
html.addEventListener('keydown', function(){
    if(isKbd === false){
        html.classList.add('isKbd');
        isKbd = true;
    };
}, true);

// 当鼠标被按下时,html 标签节点将被移除 isKbd 类名
html.addEventListener('mousedown', function(){
    if(isKbd === true){
        html.classList.remove('isKbd');
        isKbd = false;
    };
}, true);

})();
</script>

↓ View & Code ↑

这样问题就解决了吗?答案是并没有!实际上不是只有点击和键盘操作会触发聚焦,使用 JS 操作以及浏览器的一些默认行为也有可能导致触发聚焦,我们需要进一步的区分判断,并且外边框设置也会存在一些特殊情况,除了可编辑的标签可能需要特殊对待,还有一些标签如 <svg> 中的 <a> 是不能设置外边框的,设置可能并不会生效,最后就是有些标签如 <audio> 是无法监听键盘和鼠标事件的。

所以上面这个 Demo 也只是展示了一下大致的思路,距离真正的实用还有很大的一段距离,我们需要辨别出所有的可能导致聚焦的操作行为,并且还需要针对一些特殊标签进行区别对待,这实际上是一个比较复杂的问题,不是几行代码就能搞定的,作为一个普通的开发者,你未必有时间和设备去钻研这种兼容问题,所以在这种情况下推荐使用现成的开源插件来解决,而这就是下一章我们将要提到的内容了。


WICG: focus-visible 插件

业界有个 :focus-visible 伪类选择器的 polyfill 方案既 WICG - focus-visible,它的实现原理和上面那个 Demo 类似,只不过它是把类名添加到被聚焦的那个元素上,这样可以进行更加精准的控制,并且被添加的类名是 focus-visible 而不是 isKbd,其实这个方案和一般的 polyfill 还是有些差别,毕竟 CSS 伪类选择器是无法模拟的,这个方案是用了类名来代替,下面是一个简单的 Demo:

<!-- 使用 CDN 引入 WICG - focus-visible 文件 -->
<script src="https://cdn.jsdelivr.net/npm/focus-visible@5.2.0/dist/focus-visible.min.js"></script>

<style>
/* 先将 :focus 默认的所有的外边框都清理掉 */
/* 当聚焦是由于按了键盘导致时显示红色边框 */
:focus{outline:0;}
.focus-visible:focus{outline:2px solid red;}
</style>

<p>
    聚焦由点击操作触发时没有外边框,<br />
    聚焦由键盘触发时显示红色外边框。<br />
    <button type="button">button</button><br />
    <a href="javascript:void(0)">anchor</a><br />
</p>

<p>
    可输入的标签聚焦总是会显示外边框,<br />
    不管聚焦是由点击导致还是键盘导致。<br />
    <input type="text" placeholder="text" /><br />
    <span contentEditable="true">span[contentEditable="true"]</span><br />
</p>

↓ View & Code ↑

这样问题就解决了吗?答案是并没有!这只是解决了聚焦行为的判断,之后还有样式的设置问题,首先是 outline 样式的局限性,这个样式并不能实现圆角(只有 Firefox 和最新版的 Chrome 可以),除非你的项目从头到尾都没用到圆角,否则 outline 在配合圆角标签显示时总会显得很难看,其次是 <svg> 中的 <a><map> 中的 <area> 不能设置 outline 外边框,设置可能会无效。

我们可以改用 box-shadow 属性来做外边框以实现圆角,但 Safari 的表单控件并不支持这个属性,除非添加 appearance:none 的样式,但这样又会导致表单控件默认样式被破坏,而有些标签不能设置外边框,那是因为这些标签可能具有不规则的外轮廓,outlinebox-shaodw 无法处理这些不规则轮廓,所以设置可能会无效,那么多的问题太烦人了,有没有更现成的方案?有的,继续往下看。


用 xj.focus 插件来处理聚焦

XJ 自己编写了一个 xj.focus 插件,也可以当作是 :focus-visible 伪类选择器的 polyfill,但是跟 WICG 的 focus-visible 方案相比,xj.focus 插件提供了更多的 API 参数,允许你自行选择聚焦模式,并且它还自带了一个聚焦相关的 CSS 样式文件,用于解决样式的问题,如果你希望对聚焦能够有更多的细节控制或者懒得编写样式,那么 xj.focus 也是一个不错的选择,下面是一个简单的例子:

<!-- 使用 CDN 引入 xj.focus 的 JS 和 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/xjZone/xj.focus@0.4.0/dist/xj.focus.min.css" />
<script src="https://cdn.jsdelivr.net/gh/xjZone/xj.focus@0.4.0/dist/xj.focus.min.js"></script>

<p>
    聚焦由点击操作触发时没有外边框,<br />
    聚焦由键盘触发时显示蓝色外边框。<br />
    <button type="button">button</button><br />
    <a href="javascript:void(0)">anchor</a><br />
</p>

<p>
    可输入的标签聚焦总是会显示外边框,<br />
    但是这个可在全局配置中进行修改的。<br />
    <input type="text" placeholder="text" /><br />
    <span contentEditable="true">span[contentEditable="true"]</span><br />
</p>

↓ View & Code ↑

xj.focus 插件默认提供了 outlinebox-shaodw 两种外边框样式,Safari 的表单控件由于不支持 box-shaodw,所以将自动使用 outline,但你也可以通过插件的全局配置进行自由选择,其次是插件对 <svg> 中的 <a><map> 中的 <area> 聚焦都采取了无视处理,也就是不为它们设置外边框,让它们继续使用浏览器自带默认提供的外边框,同样的这个也可通过全局配置进行更改。

xj.focus 插件当然也不是完美的,由于 W3C 的一些标准的问题,所以它对 <audio><video> 的聚焦判断在部分浏览器中可能会不够精准(更多细节可参考文档),它提供的蓝色外边框可能也并不符合你项目的需求(可以复制样式代码后调整颜色进行覆盖),但总的来说它也是一个足够专业的聚焦判断插件了,如果你不想自己处理聚焦的判断问题,也不想自己处理聚焦外边框的兼容,那用它就没错啦。


关于 focus 聚焦的一些知识点

XJ 在开发 xj.focus 插件时积累下来的一些和聚焦相关的知识,如果你想进一步了解聚焦相关的内容,也许可以成为不错的参考资料。

可聚焦的标签元素列表

以下是目前发现可被聚焦的元素的列表,部分标签在不同浏览器中的表现并不一致,也许还有一些可聚焦的标签但没被发现的也不一定。

标签,备注
<a> & <area>,svg 里的 a 在 Firefox 和 Safari 中无法聚焦,并且在 IE10 中还不能设置聚焦外边框样式,设置不单不会生效,还会导致原有外边框样式失踪,可以考虑通过 "svg a {}" 选择器将这类 a 筛选出来,map 里的 area 在 IE10 中也是不能设置聚焦 outline,并不会生效且会导致原有的外边框样式失踪。
<input>,不包括 type="hidden" 的元素,因为它是 display:none;,并不会被显示出来,Safari 中 type 为 button, submit, reset, radio, checkbox, file, color, range, image 默认也是无法聚焦。
<iframe>,也许可被聚焦,但实际上 focus 事件并不会传递到父页面,:focus 伪类也不会生效。
<summary>,可聚焦的大前提是浏览器支持 details 和 summary 标签,IE18- 不支持则不能聚焦。
<audio>,audio 标签的 UI 按钮也是可以被聚焦的,但是那些按钮的样式无法被设置。
<video>,video 标签的 UI 按钮也是可以被聚焦的,但是那些按钮的样式无法被设置。
<object>,该标签在 IE10/11/18 中默认可聚焦,但是在其他浏览器中则不行。
<embed>,该标签在 IE10/11/18 中默认可聚焦,但是在其他浏览器中则不行。
<svg>,该标签在 IE10/11 中默认可聚焦,但是在其他浏览器中则不行。
<output>,该标签在 IE18 中默认可聚焦,但是在其他浏览器中则不行。
<button>,该标签在 Safari 不可聚焦,即使设置 tabIndex 也不行。
<select>,-
<textarea>,-
[tabIndex],[tabIndex]:not([tabIndex="-1"])。
[contentEditable],[contentEditable]:not([contentEditable="false"])。
CSS user-modify,在 CSS 中使用 moz-user-modify 或者 webkit-user-modify 样式让标签变得可编辑。
scrollableElement,可滚动的节点,在 Firefox 和较新的 Chrome 中都可以聚焦,但是 IE10/11/18 和 Safari 中都无法聚焦。

区分是否要添加外边框

当聚焦是 Tap 点击触发就不显示外边框,而当聚焦是通过键盘的 Tab 按键触发就显示出外边框,但实际上,情况并不总是这样单纯的。

#,描述
1,可以聚焦但不可输入的标签,如 <a><button> 和那些因为设置了 tabIndex 属性而变得可聚焦的普通标签,它们是最简单的,Tap 点击触发聚焦,就不显示外边框,通过键盘的 Tab 按键触发聚焦,就显示出外边框。
2,可以聚焦且可以输入的标签,如 <input> 标签和 <textarea> 标签,以及那些被设置了 contentEditable="true" 属性或者 user-modify 样式属性从而变得可编辑的标签,不管是由哪种操作形式触发了聚焦,都要显示出外边框。
3,可以聚焦但不能设置外边框的标签,如 <svg> 中的 <a><map> 中的 <area>,这类标签节点的外边框形状很可能是不规则的,所以浏览器也并不是通过简单的设置 CSS 的 outline 样式或 box-shaodw 样式来实现外边框,如果我们贸然的为这类标签设置外边框样式,很可能不单设置不会生效,还会导致原有的外边框失踪,尤其是 Firefox 和 IE,一旦设置了,样式将会无效,外边框也会消失,所以考虑到兼容问题,最好是不要为这类标签节点设置聚焦样式,让浏览器继续保持原状最好。
4,可以聚焦但无法判断聚焦方式的标签,如 <audio><video>,根据 W3C 的标准《media # user interface》,这两个标签的 ControlsUI 部分,点击和键盘等事件不会传递到 shadow-dom 顶层的根元素去,所以无法通过绑定事件来判断聚焦是由哪种操作导致的,这问题比较复杂,首先是 IE 和 EDGE 一直都没遵循标准,所以反而能被监听到,Safari 到目前为止(2022-11-16)也是没遵循标准,这问题从 2014 年在 Webkit Bugzilla 被提起但至今都无人理会,而 Firefox 与 Chrome 现在都已经遵循标准,所以无法监听事件,也就无法判断聚焦是由什么操作导致的,xj.focus 插件最终的做法是,IE 和 Safari 继续执行常规判断操作,Firefox84- 和 Chrome85- 将这两个标签的所有聚焦都当成是由 Tap 点击导致的,不显外边框,这会导致当使用 Tab 键聚焦时难以辨别,而 Firefox85+ 和 Chrome86+ 已经支持 :focus-visible 伪类选择器了,所以将会使用这个选择器辅助判断,就不会有问题,好在 Firefox 和 Chrome 更新换代比较勤快,低版本的 Firefox 和 Chrome 消亡得比较快,情况会逐渐好转的。

focus 事件的触发方法

目前总共发现有以下 5 种方法可以触发 focus 事件,如果聚焦回调中 event 对象可被修改,那么 event.isTrusted 属性就是 false。

#,描述
1,通过鼠标或触屏的 Tap 点击。
2,通过按键盘的 Tab 按键触发,此外还有一种方式,对于 radio 按钮控件,使用键盘的方向键,既 可将焦点切换到同 name 属性值的其他 radio 按钮控件中,此时也会触发 focus 事件的。
3,调用 Node.prototype.focus() 方法,此时 event.isTrusted 属性依旧是返回 true,因为事件虽然是由 JavaScript 引发的,但是由于事件对象并不能被修改,所以为 true,并且真的在 UI 界面上实现聚焦。
4,调用 EventTarget.prototype.dispatchEvent() 方法实现聚焦,这种触发形式的事件对象可修改,所以 event.isTrusted 属性会返回 false,由这个方法引发的 focus 事件,并不会真的在 UI 界面实现聚焦。
5,浏览器的自发行为,通常会在刷新页面或者唤醒某个页面标签后触发,这可能会伴随着 document.onvisibilitychange 事件出现,此时 event.isTrusted 属性依然返回 true,和第三种触发形式其实很相似。

参考内容

Guilherme Simões - 绝不要移除 CSS 的 outline 外边框
Caitlin Geier - 设计有用的以及可用的焦点指示器的技巧
Steve Faulkner - 如何以可访问的方式删除样式的 outline

WICG - Polyfill for :focus-visible
Lindsay Evans - Focus with Outline

W3C - Media - #user-interface

XJ.Chen - xj.focus


xjArea
12 声望0 粉丝