浅谈 :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 你就别奢望了,更详细的兼容信息可查看 MDN 或 canIuse,如果我们想要所有浏览器都支持,就得借助 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
的样式,但这样又会导致表单控件默认样式被破坏,而有些标签不能设置外边框,那是因为这些标签可能具有不规则的外轮廓,outline
和 box-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 插件默认提供了 outline
和 box-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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。