Webhek

如今你不再需要 JavaScript 来实现这些效果了,原生CSS+HTML就可以

Kilian Valkhof 发布于webhek.com

请不要对本文的标题感到反感。我不讨厌 JavaScript,我喜欢它。我每天都会写很多。但我也喜欢 CSS,我甚至喜欢 JSX HTML。我喜欢这三种技术的原因叫做:

简约至上

这是 Web 开发的核心原则之一,这意味着您应该 选择适合给定目的的功能最弱的语言

在 Web 上,这意味着更喜欢 HTML 而不是 CSS,然后是 CSS 而不是 JS。JS 是这三种语言中最通用的语言,因为你是描述浏览器应该如何行动的语言,但它也可能出问题,它可能无法加载,并且需要额外的资源来下载、解析和运行。

与命令式 JS 相比,HTML 和 CSS 是声明式的。你 告诉浏览器该 做什么,而不是 如何 做。这意味着浏览器可以选择如何做到这一点,并且可以以最有效的方式做到这一点。

由于 HTML 和 CSS 功能由浏览器处理,因此它们可以更高性能、更原生、更适应用户偏好,并且通常更易于访问。这并不意味着它会一直如此(尤其是在可访问性方面),但是当浏览器为您完成繁重的工作时,您的最终用户通常会获得更好的体验。

但我需要 JS!

你可能会想“我用 JS 做的所有事情,我 全需要 JS”。这可能是真的,但很高兴通知您,浏览器开发者和规范编写者都已经将许多功能移植到CSS和HTML中,而这些功能在几年前还需要JS。这就是本文的内容。

网络的棘手之处在于,一旦你学会了如何构建一些东西,就没有理由再学习它了。这就是我们的契约:网络是向后兼容的。(很少有例外,但第一个网页在所有现代浏览器中仍然运行良好。

这也意味着你曾经学到的解决方案会成为你工具箱的一部分,你可以继续重新实现它,每次它仍然有效。因此,我将在下面给出的例子很酷(这就是我列出它们的原因),但我希望你从这篇文章中得到的是, 仅仅因为你知道某些 东西需要 JavaScript,并不意味着它仍然需要。如果你时不时地检测这些假设,你可以开发出更好的网站。

自定义开关

我们将从我们都必须在某个时候曾经开发过的东西开始,即自定义开关。该设计没有使用常规的复选框,而是需要一个漂亮的开关。我们将使用常规复选框和 :checked 伪类,而不是使用带有 div、onclick 处理程序和内部状态的 JS 解决方案。以下是我们将要使用的 HTML:

<label>
<input type="checkbox" />
My awesome feature
</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 伪元素。这个input本身现在是我们开关的背景, ::before 伪元素是它内部执行切换的圆点。

单击此按钮仍会选中和取消选中该复选框,但由于我们替换了元素,因此我们需要自己完成使其可见的工作。这就是伪类的用武之地 :checked

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

单击该复选框时,该 :checked 伪类将开始匹配,并导致样式更新。

因此,我们有一个漂亮的自定义开关,使用原生 HTML 元素和一些 CSS,但我们还没有完成。对于鼠标用户来说,他们非常清楚他们正在与哪个表单控件交互(因为他们指向它并单击),但对于使用键盘的人来说,这并不容易。

我相信你对这部分CSS很熟悉。为了摆脱那个丑陋的、虚线的、四四方方的轮廓。

input:focus {
outline: none;
}

如果您正在阅读本文,请知道这不是一个好主意。但是我们如何让它看起来更好呢?在这里,浏览器也进行了更新,以使我们变得更好。 outline 现在遵循元素的边框半径,我们也可以将其偏移到元素之外或元素内部:

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

现在,当用户使用键盘与元素交互时(您可以尝试在单击元素后按空格键,或按 Tab 键),将匹配(使用鼠标时不会), :focus-visible 并且他们会得到一个好看的蓝色轮廓,略微围绕元素。

最后,我希望你用其他东西代替它 outline: none

input:focus {
outline-color: transparent;
}

这将产生相同的结果:轮廓不是因为隐藏而不可见,而是因为它是透明的而不可见。但是,对于打开了高对比度模式(也称为强制颜色)的用户,该轮廓将再次可见,因为在高对比度模式下,该透明颜色将替换为用户选择的颜色,从而帮助他们看到他们正在与之交互的内容,即使他们使用鼠标也是如此。

这篇文章的篇幅还不够长,无法深入探讨强制颜色的作用,但如果您想了解更多信息,请查看我的文章 强制颜色解释

Datalist,原生自动建议下拉建议列表

而不是安装 $your-framework-autosuggest,请在下一个项目中尝试 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 元素。别担心,该元素将不可见。然后,在input上使用该 list 属性将两者关联起来。

当用户现在在输入框中键入内容时,浏览器会将数据列表显示为下拉列表,并在用户键入时自动过滤选项。由于它是常规输入,因此用户仍然可以选择键入自己的值。最后,他们可以通过选择输入并使用箭头键浏览列表,或单击浏览器添加的下拉图标来查看所有选项。

功能更出色的颜色选择器

市面上有很多好看的颜色选择器,有漂亮的画布UI和滑块,都是用100行JavaScript构建的。但是您知道您也可以使用原生颜色选择器吗?

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

这一行 HTML 还为您提供了一个具有漂亮 UI 的颜色选择器,已经为您节省了一堆 JS。但是因为我们让浏览器处理它,我们实际上免费获得了更多功能。在 Chromium 浏览器中,原生颜色选择器还允许您选择颜色,不仅可以从您自己的网站,还可以从屏幕上的任何位置。很整洁!


这里需要注意的是,即使浏览器显示了一个不错的颜色选择器,您的用户也可能并非所有用户都能使用它。因此,提供一种不同的颜色选择方式(如常规文本输入)仍然是一个好主意。

折叠效果

折叠效果是一种很好的方式,可以使包含大量内容的页面更加结构化和整洁,方法是在用户需要之前将内容排除在外。浏览器通过 detailssummary 元素免费为您提供它们:

<details>
<summary>My accordion</summary>
<p>My accordion content</p>
</details>
我的折叠框

我的折叠内容

默认情况下,details元素内的所有内容都是隐藏的, 除了 summary.,当用户单击该 summary 元素时,浏览器将显示其余内容。

您经常会看到,其中一个折叠项目已经打开,其余项目已关闭。您可以使用以下 open 属性来执行此操作:

<details open>
<summary>My accordion</summary>
<p>My accordion content</p>
</details>
我的折叠框

我的折叠内容

如果你来自 React 世界,你可能会看到这段代码并认为“嗯,这太好了,现在它有了 open 道具,不会再关闭了”,但幸运的是,事实并非如此。该属性只是起始状态,当用户与折叠框交互时,该 open 属性将更新。

在外型方面,该 details 元素也可以满足您的需求。那个小三角形(你的设计师一看到它就想替换它)是一个 ::marker 伪元素,你可以设置它的样式:

summary::marker {
font-size: 1.5em;
content: "📬";
}
[open] summary::marker {
font-size: 1.5em;
content: "📭";
}
我的折叠框

我的折叠内容

请记住,更改内容可能会影响辅助技术展开折叠的方式。在此处阅读 Manuels 的文章: 详细信息/摘要不一致。此外,对于Safari,您必须使用 ::-webkit-details-marker 伪元素。

标记伪元素不能像其他元素那样广泛地设置样式(许多 CSS 属性不适用于它,例如将其放置在完全不同的位置),但您可以替换其内容,例如用表情符号,或设置背景颜色或图像并更改其字体大小。

使用该 open 属性,您可以轻松地为其提供与关闭状态不同的样式。

最后,我们想对这个summary元素做一些事情。它是可点击的,但与链接不同的是,它没有指针光标,而且与按钮不同,它看起来不像按钮。所以我认为我们应该给它添加一个悬停和焦点状态,并帮助我们的访问者意识到它是可点击的:

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。

dialog 元素具有它公开的 showModal() 函数,您可以使用它打开对话框。此对话框在称为 上 top-layer打开,这是浏览器中的一个新概念。如需入门,请查看 MDN 上的解释器: 顶层

顶层是独立于 HTML 的新层,您可以将元素“提升”到其中。这意味着顶层上的元素将始终高于其他所有元素,而不管元素的 z-index 和堆叠上下文嵌套如何。

不过,现在它已经打开了,您可能会注意到浏览器没有为您提供任何 UI。该对话框几乎是一个 div(而不是按钮!),由您来提供关闭的 UI。这就是上面代码中的表单的作用。您可能已经注意到它有一个“对话”方法。提交此表单后,浏览器会将其视为再次关闭对话框的信号。

有了它,您还可以通过提供两个按钮来创建确认对话框,每个按钮都有自己的值:

<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,所以你可以随心所欲地设置它的样式。浏览器会自动将其放置在屏幕中间,但其他一切都取决于您。

Dialog 还附带了一个名为 ::backdrop的新伪元素。这是位于对话框和页面其余部分之间的层,您可以设置其样式,例如使页面的其余部分变暗或以其他方式将用户的注意力引导到对话框上。例如,您可以叠加白色图层并模糊页面:

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

这是一个带有样式背景的对话框

就像对话框元素本身一样,背景是由浏览器定位的,因此您无需担心滚动、固定元素和浏览器大小调整。这一切都由浏览器为您处理。

结束语

我希望你在这篇文章中发现了一些有用的东西,让你意识到你可以在下一个项目中少用一点javascript。每当您将已知的经过实战考验的实现更改为新实现时,最好对其进行测试,尤其是在可访问性方面,以确保您不排除任何人。

我还可以在本文中添加更多示例,以下是您可以查看的一些示例:

  • 本机平滑滚动 scroll-behavior: smooth (但仅在匹配时 prefers-reduced-motion: no preference
  • 带有滚动捕捉的本地轮播,
  • “视图内”元素 position: sticky
  • ...更不用说容器查询的整个概念了。

如果我们展望未来,我们会得到更多很酷的东西:

  • 滚动驱动的动画
  • 不需要masonry.js实现masonry布局 grid-template-rows: masonry
  • 使用新selectlist元素完全风格化select(您可以在其中设置选择的每个部分的样式,而不会破坏它附带的所有本机功能)
  • :has()将消除一整类 JS 选择的选择器

本文改编自我的会议演讲,更详细地介绍了这些主题和其他主题,您可以在此处观看: 停止使用 JavaScript:将功能从 JS 移动到 CSS 和 HTML。

因此,让我重申本文的主要观点:

仅仅因为 你知道 某些东西需要 JavaScript,并不意味着它仍然需要。如果你时不时地测试这些假设,你可以制作更好的网站。