cover

纯 CSS 实现 UI 组件的触发

问题

在实现 UI 组件的时候,有时候需要借助 JavaScript 实现状态触发,例如 Dropdown,Dialog。在我自用的 Material UI 库中,之前是借助 Stimulus 框架实现的,它的 HTML 内容是这样:

<div class="dropdown" data-controller="dropdown">
  <button type="button" data-action="dropdown#toggle">Button</button>
  ...
</div>

JS 内容只是添加一个 CSS class:

// dropdown_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  toggle() {
    this.element.classList.toggle('dropdown--open')
  }
}

在使用的时候,必须引用库的 JavaScript 部分,这对不使用 Stimulus 的项目并不友好,因为引入了额外依赖。

对于这些简单的 UI 触发逻辑,如果能用纯 CSS 实现就更好了。

解决方案

daisyUI 是一个 Tailwind CSS 的组件样式插件,我在浏览的时候发现它的 UI 组件并不依赖 JavaScript,于是我参考它的实现,实现了纯 CSS 的触发。以下记录解决的过程。

纯 CSS 的 Dropdown 实现

Dropdown 的需求是用户点击按钮的时候展示弹出菜单,用户点击按钮和菜单以外的地方时隐藏菜单。这种需求可以通过 CSS 的 :focus-within 选择器实现。

:focus-within 是一个CSS 伪类 ,表示一个元素获得焦点,或,该元素的后代元素获得焦点。换句话说,元素自身或者它的某个后代匹配 :focus 伪类。MDN Doc

将 Dropdown 的 HTML 改为为:

<div class="dropdown">
  <label tabindex="0" class="button button--filled">Dropdown button</label>
  <div tabindex="0" class="dropdown__container">
    ...
  </div>
</div>

其中 label 和 div 的 tabindex 属性是必须的,这让这两个元素成为可以 focus 的目标。

之所以用 label 而不用 button,是因为 Safari 的 bug 无法 focus button。

CSS 关键的内容如下:

.dropdown__container {
  visibility: hidden;
}

.dropdown:focus-within .dropdown__container {
  visibility: visible;
}

用户点击 label 或者 container 的时候,.dropdown 会触发 :focus-within 样式,从而展示菜单内容。

使用 :focus-within 还有另外一个好处,那就是通过键盘 Tab 键也能触发 dropdown。

纯 CSS 的 Dialog 实现

Dialog 的需求和 Dropdown 类似,但触发条件不止一个,可以是 dialog 以外的 button,或者 dialog 内的背景层或者 button。这时候 :focus-within 就不够用了。

这使用可以使用额外的 input checkbox 作为触发元素,:checked 作为触发条件。

:checked CSS 伪类选择器表示任何处于选中状态的 radio (<input type="radio">), checkbox (<input type="checkbox">) 或 ("select") 元素中的option HTML 元素 ("option")。MDN Doc

Dialog 的 HTML 内容如下:

<label for="dialog-toggle" class="button button--filled">
  Dialog Toggle
</label>

<input type="checkbox" id="dialog-toggle" class="dialog-toggle">
<div id="demo-dialog" class="dialog">
  <div class="dialog__container">
    <label for="dialog-toggle" class="button button--text">
       Cancel
    </label>
  </div>
  <label for="dialog-toggle" class="dialog__scrim"></label>
</div>

这里利用了 labelfor 属性切换 checkbox 的状态,注意用于触发的 label 可以放在 Dialog 外部或内部,并且样式也可以任意定义,可以显示为一个按钮,也可以显示为背景层。

CSS 关键的内容如下:

.dialog {
  visibility: hidden;
}

.dialog-toggle:checked + .dialog {
  visibility: visible;
}

这样在 .dialog-toggle checked 状态切换的时候,dialog 的样式也会切换。

讨论

完成改造后,Material UI 库是一个纯 CSS 的 UI 库了,不需要引用 JS 部分。

本文只展示了关于触发状态的代码,需要完整代码可以查看项目源码:https://github.com/chloerei/material-ui

如果知道其他用 CSS 替代 JavaScript 的技巧,欢迎在评论区留言。

9
4
2