CSS Sticky Hover and Pseudo States

BACKGROUND

接触过 CSS 的人应该都熟悉 :hover 伪类。其最常用的场景为鼠标经过并悬停 (hover) 于该元素之上时,元素样式发生的变化。

例如,当我们指定如下 CSS 代码:

1
2
3
4
5
6
7
button {
opacity: 1;
}

button:hover {
opacity: 0.9;
}

在不存在其它干扰的情况下,这段代码会让 HTML 中的 button 元素在鼠标悬停时,透明度从 1 变为 0.9。

:hover 伪类很重要,因为它向用户即时地传达了哪些元素是可操作 / 可点击的,而哪些元素没有这些交互功能。 cursor: pointer 通常和 :hover 伪类的样式变化一起使用,以表达更加完善、丰富的交互反馈。

问题在于,传统的 Web 浏览方式是通过键鼠交互的桌面设备。自 2007 年以来,智能手机开始成为越来越流行的 Web 浏览设备。很显然,在触屏设备中,并没有能恰当对应键鼠操控中 hover 的动作。

但如果你平时曾有过注意,便会发现各个智能手机上的浏览器都不约而同地采用了「手指点按」作为触发 :hover 伪类样式的 trigger。

触屏设备上的具体表现是:

在手指点按一个元素时,同时激活元素的 :hover:active在手指抬离屏幕或移向元素之外时,元素的 :active 状态被摘下,但 :hover 状态被持续保留,直到屏幕范围内有另一个元素被点按。

此逻辑与另一个 CSS 动态伪类 :focus 非常相似。在这里,「屏幕范围内有另一个元素被点按」对应着 :focus 失去焦点被摘下的表现。而「 :hover 状态在点按结束后被持续保留」,就是我们所说的在触屏设备上的 sticky-hover(粘滞性悬停)特点。

STICKY-HOVER

也许有个人的感情色彩,但 sticky-hover 是一个非常讨厌的现象。

我们在点按一个按钮并抬起时,总是希望这一动作是干脆、利落地完成,而粘滞的 :hover 样式会给人上一个动作还未做完的感觉。屏幕上留下的粘滞样式,让人感觉心情不适,很影响视觉体验。

许多用户所感受到的「移动端 Web 不如 Native 应用交互舒服」,一个重要的因素便是 Web 在移动端表现的粘滞悬停。在 StackOverflow 上,也有过关于如何禁用移动端粘滞悬停的讨论

事实上,在 W3C 制定相关标准时,也并未表现过支持这种处理方式的态度。以下是标准中关于 CSS 动态伪类的定义性解释

Interactive user agents sometimes change the rendering in response to user actions. Selectors provides three pseudo-classes for the selection of an element the user is acting on.

The :hover pseudo-class applies while the user designates an element with a pointing device, but does not necessarily activate it. For example, a visual user agent could apply this pseudo-class when the cursor (mouse pointer) hovers over a box generated by the element. User agents that do not support interactive media do not have to support this pseudo-class. Some conforming user agents that support interactive media may not be able to support this pseudo-class (e.g., a pen device that does not detect hovering).

The :active pseudo-class applies while an element is being activated by the user. For example, between the times the user presses the mouse button and releases it. On systems with more than one mouse button, :active applies only to the primary or primary activation button (typically the “left” mouse button), and any aliases thereof.

The :focus pseudo-class applies while an element has the focus (accepts keyboard or mouse events, or other forms of input).

在解释 :hover 伪类适用的激活范围时,W3C 特意指出:在某些不支持悬停侦测的交互模式下,如一台使用触控笔交互的设备,这一伪类无法被支持。而手指触屏和笔触屏本质上是相同的,因此一台符合标准的设备 (Conforming user agent) 不应当实现 :hover 的状态激活。

但为何所有的触控浏览器实现厂商,都不约而同地违反这一标准呢?

我能够想到的一个原因,即使样式表现的完备性 (completeness)。

STYLES COMPLETENESS

我们仍然假设一个 button,分别具有常态、:hover 激活和 :active 激活三种不同的样式表现:

1
2
3
4
5
6
7
8
9
10
11
button {
opacity: 1;
}

button:hover {
opacity: 0.9;
}

button:active {
transform: scale(1.1);
}

很明显,在键鼠控制的传统 Web 浏览设备中,button 具有三层样式,分别是常态、:hover 激活、:hover:active 同时激活:

这三层样式是传统 Web 开发者对一个元素的全部「期待样式 (expected styles)」:即我希望这三种样式都可以被激活,不需要其它可能的组合被激活。

如图中可见,这三种样式状态可以被鼠标悬停进入 (mouseOver)、鼠标按下 (mouseDown)、鼠标抬起 (mouseUp) 和鼠标移出 (mouseOut) 等时间分离开来。

W3C STANDARD

在 W3C 标准中,如果真的完全移除对 :hover 伪类的支持,那么触屏设备上可能触发的样式集合只剩下两种,从而不具备这种完备性:

Fig 2. W3C 标准下的触屏设备交互实现

如果说仅仅对于样式上完备性未满足产生的折扣尚且可以忍受,那么功能上的呢?

事实上,:hover 伪类在 Web 上除去样式外确实存在一定功能触发作用。如:

1
2
3
4
5
6
7
nav .menu {
visibility: hidden;
}

nav:hover .menu {
visibility: visible;
}

如此实现是否优雅得体,我们尚且搁置。但现在的 Web 上,确实有很多类似的写法存在:你需要鼠标悬停某一处,才可以展开一个特定的菜单或是触发其它视图。

如果 :hover 伪类彻底不被实现,那么网站在该设备上的功能就是不健全的。这一点在当时,对于触屏设备这种新生事物,显然无法忍受。

在完备性没有得到保证之外,该模式还存在一个问题,即它可能会触发一个预料外样式 (unexpected style)::active 被激活而 :hover 未被激活。具体到本例中,即是 button 不透明但却被放大到 1.1 倍的样式。

我们知道,由于在桌面设备下 :active 总是在 :hover 已激活的情况下才会激活,所以这一样式是开发者未预料到的。

这并非一个太大的问题,但它确实是存在且可能产生影响的。

CONVENTION

因为上述的这些问题,才有了今天触屏设备在惯例上对 :hover 的支持和触发逻辑:

Fig 3. 触屏设备的惯例实现

在这种模式中,样式保持了传统 Web 中三层样式的完备性,即所有的期待样式均可被触发,同时又没有未期待的样式被触发。它是完备的,没有疑问;但它同时又是丑陋的,因为这样处理将产生不可避免地粘滞性悬停(如图),影响交互体验。

A THIRD OPTION?

事实上,除去以上两种处理方案之外,还有一种可能:

Fig 4. 另一种可能

这一种模式与 W3C 标准只有一处不同,即它规定了 :hover 必须和 :active 同步触发、同步结束。

它和目前惯例采用的模式也只有一处不同,即移除了形成粘滞性悬停的阶段。

它解决了视觉上不适的问题,显然。此外,它也解决了 W3C 标准中「预料外样式」的问题。

但它还是缺少完备性,即用户没有办法控制 :hover 单独出现。而且,用户也没有对 :hover 状态进行保持的方便性 —— 如在前面举过的 nav – menu 例子中,用户只能在手指按住 nav 的同时选取 menu。

看起来操作很别扭!

MY CHOICE

事实上,第三种方案是我在开发中更加倾向的方案。

确实,从整个 Web 生态看来,可能它存在的问题比如今浏览器采用的模式更大。但对于个人和团队新开发的项目,如果应用一定的交互设计理念和标准,便可以将它的影响缩至最小。

首要的一点,便是页面中最好不要有上文中 nav – menu 例子中的悬停显示交互模式。虽然有人喜欢,但我个人是反感这种交互逻辑的。

最常见的悬停显示,是 Windows 旧版本的开始菜单。为了进入下一级菜单,你需要持续将鼠标悬停在本级菜单的选项中,小心翼翼地平移过去。如果你使用触控板,甚至会比用鼠标小心地移动更加痛苦。

这种交互模式的设计初衷可能是为了省下一次点击,但事实证明,一次点击所带来的交互代价相比谨慎的移动操作,是微乎其微的。

如果悬停元素的面积足够大或许还好,但这种叠式布局出现的意义,就是为了节省空间。

京东的主页分类栏目中,所运用的就是这种交互。它们现在仍然在采用,但为了弥补其缺陷,团队写了大量的、复杂的 JS 代码用于判定用户鼠标轨迹的特征,从而推测用户意向。如图所示,当用户从一级菜单中径直、快速地指向二级菜单时,尽管划出了一级菜单允许的悬停区域,显示的内容仍然不会发生改变。

Fig 5. 京东主页的悬停显示交互

但是这样做完全不简洁,且不足够优雅。无论怎么写推测用户意向的逻辑,总是会有误判的情况。

悬停的 :hover 作用,应该是样式在常态和 :active 状态下的过渡。依赖于悬停交互展开菜单的功能,很可能本身就不是好的交互模式。

对于其它的悬停功能,如 Tooltip 等,在此种方案下保留了和惯例模式一样的表现,即长按时触发 —— 虽然在移动端,Tooltip 本身也不是理想的交互组件。

CONCLUSION

在 Web 开发领域,前端背负的历史包袱是多而重的。

版本的迭代必须保证以往的所有 Web 页面正常运行,因此,现在的 Web 标准存在许多永久性的妥协。CSS 的 border-box,和 JavaScript 中的 typeof null,都是很典型的例子。而文中所讨论的触屏设备交互逻辑适配,实质也是一种妥协。

好消息是,现在 Web 前端似乎比任何一个 IT 技术领域都更具有活力。

我们有理由寄希望于 Web 社区,为这些历史遗留问题,不断带来更加优雅、健壮的解决方案。

+