简单而强大:这些场景无JavaScript也能轻松应对

作者 | kilian valkhof
译者 | 许学文
策划 | 丁晓昀

请不要因为文章的标题而对我心怀敌意。对于 javascript,我只有喜爱没有讨厌,与此同时,我每天还会编写大量的 javascript 代码。不过,除了 javascript 之外,css 和 html 也都是我非常喜爱的技术。我之所以偏爱这三种技术是有些原因的。

最小能力原则

web 开发的第一核心原则就是“最小能力原则”。换而言之,也就是当我们实现目标时,应该 选择相对较轻量的编程技术去实现

依据此原则,也就意味着在 web 开发中,对于相同功能的实现,开发者应该优先考虑 html,其次是 css,最后才是 js。由于 js 使开发者可以控制浏览器的行为,所以 js 在三种技术中使用的最广泛。不过,js 也会因为诸如外部资源加载失败、解析异常、执行错误等原因,从而导致其对浏览器的控制失效。此外,js 对诸如依赖键盘、依赖额外辅助设备等对可访问性有要求的用户的支持也不够友好。

与命令式的 js 不同,html 和 css 是声明式的。因此与使用 js 的情况不同,在使用 html 和 css 时,开发者是告诉浏览器做什么,而不是怎么做。这也就意味着浏览器可以自己选择怎么做,从而以最高效的方式实现。

由于 html 和 css 的各项功能都是浏览器原生支持的,因此这些功能通常会有更好的性能、更原生、具备更好的用户体验,对可访问性的支持也更好。虽然并非所有的场景都优于 js(特别是在可访问性方面),但大多数时候,利用浏览器原生功能来实现复杂逻辑,会给用户带来更好的使用体验。

但是,我离不开 js

也许你会想:“我用 js 实现的所有功能,都需要 js 才能实现。”这种想法可能是对的,但值得注意的是,无论是浏览器制造商还是规范编写者,他们已经将许多功能转移到了 css 和 html 上,这些功能在几年前还必须使用 js 才能实现。这就是本文要讨论的内容。

web 开发中的一个棘手之处在于,一旦你学会了如何构建某个东西,就再也没有必要重新再学一次。因为这是 web 开发行业达成的常识约定:web 是向后兼容的(虽然有极少数例外情况,但第一个网页在当前所有现代浏览器上仍然可以正常运行)。

这也就意味着,你所学到的实现方案将会成为你工具箱中的一部分,你可以一直重复实现它,并且每次都能正常运行。所以我接下来会给出一些比较酷的示例,希望文章中的这些示例让你能明白,那些当初你认为必须通过 javascript 才能解决的问题,其实并不一定现在仍然需要(这也是我为什么列举这些示例的原因)。所以,如果你能时不时尝试这些设想,或许你能开发出更好的网站。

自定义开关

自定义开关,是所有 web 开发人员都不可规避的一个功能,那么我们就从如何实现一个自定义开关来开始本文。设计师的需求是实现一个漂亮的开关功能而不是一个复选框。与使用 div、onclick 事件处理程序和内部状态的 js 解决方案不同,这里我们将通过普通的 checkbox 和:checked 伪类来实现。下面是我们将要用到的 html 代码:

<label>  <input type="checkbox" />  my awesome feature</label>

在上面的代码中有一个 label 元素,里面包含一个复选框。这样做的好处是浏览器会默认为我们做一些事情。由于输入框位于 label 标签的内部,因此浏览器会将它们关联起来,所以现在我们在 label 标签的任何位置进行点击,都能操作复选框的切换,而无需任何 onclick 事件的处理程序。浏览器免费为我们实现了这个功能。如果单单就功能而言,我们就已经完成了。

当然,上面这种外观,设计师可能不喜欢,所以我们需要创建一个外观开看起来还不错的自定义开关。那么接下来,让我们给自定义开关添加一些 css:

input {  appearance: none;  position: relative;  display: inline-block;  background: lightgrey;  height: 1.65rem;  width: 2.75rem;  vertical-align: middle;  border-radius: 2rem;  box-shadow: 0px 1px 3px #0003 inset;  transition: 0.25s linear background;}input::before {  content: "";  display: block;  width: 1.25rem;  height: 1.25rem;  background: #fff;  border-radius: 1.2rem;  position: absolute;  top: 0.2rem;  left: 0.2rem;  box-shadow: 0px 1px 3px #0003;  transition: 0.25s linear transform;  transform: translatex(0rem);}

上面样式的具体细节并不重要,但我希望你注意第一条样式规则:appearance: none。

表单元素和图像元素都属于一种被称为“替换内容”的元素。这意味着,在 html 中这些元素的内容是由浏览器提供的,而非标签本身。在浏览器渲染 html 过程中,当发现替换内容时,它会为其留下一个盒子,然后会用实际内容来替换该盒子。这就是为什么诸如图像、表单等 “替换内容”元素不能有伪元素的原因:当浏览器把整个元素替换掉时,这些伪元素也会同时被替换掉。

给元素设置 appearance,是禁止浏览器这种行为的方式之一。它告诉浏览器:“谢谢,但我想自定义我的表单控件样式”。这样我们就可以使用::before 伪元素了。现在输入框本身就是我们开关的背景,而::before 伪元素就是其中负责切换功能的小圆点。

点击仍然会触发复选框选中和未选中的切换,但由于我们替换了元素,所以选中和未选中的状态展示需要我们自己处理。因此我们引入:checked 伪类来实现此功能:

:checked {  background: green;}:checked::before {  transform: translatex(1rem);}

当你点击复选框时,:checked 伪类开始匹配,从而实现样式的更新。

到目前为止,通过原生的 html 元素和一些 css 我们创建了一个外观漂亮的自定义开关,但事情还没完。因为,对于使用鼠标的用户来说,可以很明显的知道他们正在与哪个表单控件进行交互(因为他们可以指向并点击),而对于使用键盘的用户来说,情况就不那么容易了。

译注:下图是使用键盘操作的时候出来的样式:

我相信你对下面这段 css 代码会很熟悉。这段 css 代码是为了将那个丑陋的、虚线的、方形的外边框去掉。

input:focus {  outline: none;}

如果你正在阅读这篇文章,那么后面会知道这不是一个好的实现方式。但是我们如何才能让它看起来更好呢?在这方面,浏览器也进行了更新,为我们提供了更好的体验。现在元素边框的圆角设置同样会在外边框上生效,并且我们还可以将其偏移或嵌入到元素内部:

input:focus-visible {  outline: 2px solid dodgerblue;  outline-offset: 2px;}

现在,当用户使用键盘与元素进行交互(你可以尝试在点击后按下空格键,或者使用 tab 键切换到它),:focus-visible 会生效(使用鼠标时不会),并且它们会在元素周围显示一个好看的、蓝色的轮廓线。

最后,我希望你将上面那个 outline: none 替换为如下内容:

input:focus {  outline-color: transparent;}

两个 css 代码都将产生相同的效果:这里是通过将外边框颜色设置为透明,而不是通过隐藏外边框来实现的。这样做的好处是,对于打开了高对比度模式(也称为强制颜色)的用户而言,此时外边框会再次显示出来,因为在高对比度模式下,透明颜色会被用户所选择的颜色替代,从而帮助他们看清楚正在与之交互的内容,即使他们使用鼠标也一样有效果。

本文篇幅不足以详细介绍强制颜色的功能,但如果你想了解更多,请查看我的文章 《强制颜色解析》(https://polypane.app/blog/forced-colors-explained-a-practical-guide/)。

datalist,
一个原生的自动输入建议元素
在你的下一个项目中,不要安装任何实现自动输入建议的框架,尝试使用一下 datalist 元素。该元素是浏览器内置的一种实现用户在输入框中输入时,自动以数据列表显示输入建议的方式。
<input list="frameworks" /><datalist id="frameworks">  <option>bootstrap</option>  <option>tailwind css</option>  <option>foundation</option>  <option>bulma</option>  <option>skeleton</option></datalist>

你可以通过在 html 中添加一个带有 id 属性和一组选项值的 datalist 元素来使用它。不用担心,该元素默认是不可见的。然后,你需要在输入框上通过设置 list 属性来将两者关联起来。使用效果如下:

当用户在输入框中输入时,浏览器会将 datalist 显示为下拉列表,并根据用户输入自动过滤选项。由于它本质上依然是一个普通的输入框,所以用户仍然可以输入自定义值。最后,用户可以通过选择输入框来查看数据列表,并通过箭头建进行数据导航。当然,用户也可以通过点击浏览器默认添加的下拉图标来查看所有的数据选项。

功能丰富的颜色选择器

市面上有很多外观漂亮的颜色选择器,这些颜色选择器除了拥有漂亮的画布界面之外,还有通过上百行 js 代码实现的滑块功能。但是你知道吗?其实你可以使用浏览器原生的颜色选择器。

<label> <input type="color" /> color </label>

这一行 html 代码就可以给你一个带有漂亮界面的颜色选择器,这就已经节省了大量的 javascript 代码。除此之外,因为我们将实现让给了浏览器来处理,所以实际上我们还可以免费获得更多功能。在 chromium 浏览器中,这个原生的颜色选择器还可以让你从屏幕上的任何位置选择颜色,这真的是太棒了!

不过这里值得注意的一点是,虽然浏览器显示了一个漂亮的颜色选择器,但不一定你的所有用户都可以使用它。因此提供其他选择颜色的方式(比如常规文本输入)仍然是一个好的选择。

手风琴(accordions)

accordions 是一种很好的帮助我们更好地组织页面上的大量内容,使其结构更清晰,避免展示杂乱无章的方式。而浏览器也已经为我们提供了免费的实现方式,开发者通过使用 details 和 summary 元素来实现:

<details>  <summary>my accordion</summary>  <p>my accordion content</p></details>

默认情况下,details 元素中除了 summary 元素之外,其余元素内容都是隐藏的。当用户点击 summary 元素时,浏览器才会显示其余的内容。

通常情况下,在一组 accordions 中,会有一个默认处于展开状态,而其他的则默认处于收起状态。这一点,你可以通过使用 open 属性来实现:

<details open>  <summary>my accordion</summary>  <p>my accordion content</p></details>

如果你是 react 使用者,可能会认为:“太好了,现在它有了 open 属性,就再也不会收起了。”但还好并非如此。open 属性只是初始状态,当用户与 accordions 进行交互时,它会根据用户的操作自动进行更新。

在样式上,details 元素也为我们提供了解决方案。那个小三角形(一旦设计师看到它,就会立刻想要替换掉)是一个伪元素::marker,你可以对其进行样式设置:

summary::marker {  font-size: 1.5em;  content: "";}[open] summary::marker {  font-size: 1.5em;  content: "";}

但值得注意的是,更改内容可能会影响辅助技术对你的 accordions 的解析。你可以阅读 manuels 的文章 《details/summary 的一致性问题》(https://www.matuzo.at/blog/2023/details-summary) 来了解更多。此外,对于 safari 浏览器,你需要使用::-webkit-details-marker 伪元素来处理浏览器差异性问题。

伪元素::marker 的样式设置选项相对有限(许多 css 属性对其不起作用,例如无法将其完全定位到不同的位置)。但你可以替换其内容,例如使用表情符号,或设置背景颜色或图像,并更改其字体大小。

通过 open 属性,你可以轻松地分别为展开状态和收起状态设置不同的样式。

最后,我们需要对 summary 元素进行一些处理。虽然它是可点击的,但其与 a 链接元素和按钮元素都不同;当鼠标悬停的时候,它既没有指针光标,也没有类似按钮那样的 hove 态。因此,我认为我们应该为它添加鼠标悬停和焦点状态,以帮助访问者可以意识到它是可点击的:

summary:hover,summary:focus {  cursor: pointer;  background: deeppink;}

在这里,我不想进行“只有链接才应该具有指针光标”的讨论,我的主要观点是你需要做一些处理来提醒用户。

对话框

有时候,你需要向用户展示一些信息,或者询问他们问题,或者让他们确认某些事情。在 javascript 中,这就是 alert()、prompt() 和 confirm() 的作用。但它们有一个很大的问题:它们会锁定主线程。这意味着你的页面无法做其他任何事情。而且它们是浏览器原生的,无法根据你的设计进行样式定制化。

如果你自己构建一个对话框,也会遇到一些麻烦:为了支持可访问性,你需要确保对话框内部能获得焦点,此外还需要声明它是模态对话框,以确保用户无法意外关闭它,最后还要与可能设置了 z-index 为 2147483647 的聊天小部件进行斗争(如果你知道这些小部件的存在的话)。

因此,现代浏览器为我们提供了原生的对话框元素:

<dialog>  <form method="dialog">    <h3>this is a pretty dialog</h3>    <button type="submit">close</button>  </form></dialog>

默认情况下,对话框元素不会显示出来。现在,我会稍微作弊一下,通过 javascript 来控制其显示和隐藏:

document.queryselector("button").addeventlistener("click", () => {  document.queryselector("dialog").showmodal();});

虽然行业正在推动不依赖 javascript 来控制对话框的显示,有一些工作正在进行中,但它们尚未完全规范化,更不用说实现了。所以目前,我们仍然需要使用 javascript 来打开对话框。但仅此而已,其余的都是原生的 html 和 css。

对话框元素具有一个叫做 showmodal() 的函数,通过它来打开对话框。对话框会打开在一个称为 top layer 的东西上面,这是浏览器中的一个新概念。如果你想了解更多信息,请参阅 mdn 上关于 top layer 的解释。

top layer 是一个与你的 html 分离的新层,你可以将元素“提升”到这个层级。这意味着,不管元素的 z-index 和堆叠上下文嵌套如何,位于 top layer 的元素将始终在其他所有元素之上。

你可能会注意到浏览器并没有给打开状态的对话框提供任何用户界面。对话框实际上只是一个 div 元素(不是按钮!),因此你需要自己为对话框提供用于关闭的用户界面。这就是上面代码中的表单元素的作用。你可能已经注意到,该表单的 method 属性值是"dialog"。此时,当该表单被提交时,浏览器会将其视为关闭对话框的信号。

通过这种方式,你还可以创建确认对话框,提供两个按钮,每个按钮都有自己的值。

<dialog>  <form method="dialog">    <p>tabs or spaces?</p>    <button type="submit" value="wrong">tabs</button>    <button type="submit" value="correct">spaces</button>  </form></dialog>

可以通过监听对话框的 close 事件来处理用户的按钮点击,并通过“returnvalue”属性来获取对应按钮的值。

dialog.addeventlistener("close", function () {  console.log(dialog.returnvalue);});

如果对话框中还有其他表单数据,你也可以以 formdata 的方式读取它们。

由于对话框本质上是一个 div 元素,你可以根据自己的喜好进行样式设置。浏览器会自动将其居中显示在屏幕上,但其他所有内容你都可以自定义。

除此之外,对话框还提供了一个称为::backdrop 的新伪元素。它位于对话框和页面其他部分之间,你可以对其进行样式设置,例如调暗页面的其他部分或以其他方式引导用户关注对话框。例如,你可以添加一个白色覆盖层并模糊页面。

dialog::backdrop {  background: #fff5;  backdrop-filter: blur(4px);}

与对话框元素本身一样,背景层也是由浏览器自动进行定位。因此你不需要担心页面滚动、固定元素和浏览器大小调整等情况。这一切都由浏览器自动处理。

最后

通过这篇文章,我希望你从中能发现了一些让你在下一个项目中可以少用一点 javascript 的东西。不过,每当你将一个经过大量实战验证的实现方式改成新的方式时,最好进行测试,特别是涉及到可访问性的时候,以确保你不会忽略任何用户。

下面这些是我没有加到本文中的示例:

  1. 使用 scroll-behavior: smooth 实现原生平滑滚动(但仅当 prefers-reduced-motion: no preference 匹配时才能生效),

  2. 使用 scroll-snap 实现原生轮播图,

  3. 使用 position: sticky 实现“视图内固定”的元素,

  4. ……以及所有的和容器查询相关的示例。

而且,如果我们展望未来的话,我们将会有更多很酷的东西:

  1. 滚动驱动的动画

  2. 使用 grid-template-rows: masonry 替代 masonry.js 实现砌砖布局

  3. 使用新的 selectlist 元素实现完全可样式化的选择框(你可以对选择框的每个部分进行样式设置,而不破坏其所带来的所有原生功能)

  4. :has() 选择器将消除一整类 javascript 选择器

这篇文章是我在一个会议上做的演讲的改编版本,如何想更详细地了解关于本文内容的介绍或其他主题,你可以在观看视频:不要再通过 javascript 实现它们:迁移 js 功能到 css 和 html(https://www.youtube.com/watch?v=ztmuju26b7q)

所以,让我再次强调这篇文章的主要观点:

仅仅因为你知道某个功能需要 javascript,但并不意味着它仍然需要。如果你不时地测试这些尝试,你可以制作出更好的网站。

https://www.htmhell.dev/adventcalendar/2023/2/

声明:本文为 infoq 翻译整理,未经许可禁止转载。