纯 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>
这里利用了 label
的 for
属性切换 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 的技巧,欢迎在评论区留言。