当我第一次发现 Material Design 时,我特别受到其 按钮组件 的启发。它使用涟漪效果以简单优雅的方式为用户提供反馈。
这种效果是如何工作的?Material Design 的按钮不仅仅带有整洁的涟漪动画,而且动画还会根据点击按钮的位置而改变位置。

我们可以达到相同的结果。我们将从使用 ES6+ JavaScript 的简洁解决方案开始,然后查看一些替代方法。
HTML
我们的目标是避免任何多余的 HTML 标记。因此,我们将使用最少的标记
<button>Find out more</button>
按钮样式
我们需要使用 JavaScript 动态设置涟漪的一些元素的样式。但其他所有内容都可以在 CSS 中完成。对于我们的按钮,只需包含两个属性即可。
button {
position: relative;
overflow: hidden;
}
使用 position: relative
允许我们在涟漪元素上使用 position: absolute
,我们需要控制其位置。同时,overflow: hidden
防止涟漪超出按钮的边缘。其他所有内容都是可选的。但现在,我们的按钮看起来有点过时了。这是一个更现代的起点
/* Roboto is Material's default font */
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
button {
position: relative;
overflow: hidden;
transition: background 400ms;
color: #fff;
background-color: #6200ee;
padding: 1rem 2rem;
font-family: 'Roboto', sans-serif;
font-size: 1.5rem;
outline: 0;
border: 0;
border-radius: 0.25rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3);
cursor: pointer;
}
涟漪样式
稍后,我们将使用 JavaScript 将涟漪作为带有 .ripple
类的跨度注入到我们的 HTML 中。但在转向 JavaScript 之前,让我们在 CSS 中定义这些涟漪的样式,以便我们随时准备使用它们
span.ripple {
position: absolute; /* The absolute position we mentioned earlier */
border-radius: 50%;
transform: scale(0);
animation: ripple 600ms linear;
background-color: rgba(255, 255, 255, 0.7);
}
为了使我们的涟漪呈圆形,我们已将 border-radius
设置为 50%。为了确保每个涟漪都从无到有出现,我们已将默认比例设置为 0。现在,我们将无法看到任何内容,因为我们还没有为 top
、left
、width
或 height
属性提供值;我们很快就会使用 JavaScript 注入这些属性。
至于我们的 CSS,我们需要添加的最后一件事是动画的结束状态
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
注意,我们没有在关键帧中使用 from
关键字定义起始状态?我们可以省略 from
,CSS 将根据应用于动画元素的值构建缺失的值。如果显式声明相关值(如 transform: scale(0)
)或如果它们是默认值(如 opacity: 1
),则会发生这种情况。
现在是 JavaScript 的时间了
最后,我们需要 JavaScript 来动态设置涟漪的位置和大小。大小应基于按钮的大小,而位置应基于按钮和光标的位置。
我们将从一个空函数开始,该函数以点击事件作为参数
function createRipple(event) {
//
}
我们将通过查找事件的 currentTarget
来访问我们的按钮。
const button = event.currentTarget;
接下来,我们将实例化我们的跨度元素,并根据按钮的宽度和高度计算其直径和半径。
const circle = document.createElement("span");
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
我们现在可以定义涟漪所需的其余属性:left
、top
、width
和 height
。
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (button.offsetLeft + radius)}px`;
circle.style.top = `${event.clientY - (button.offsetTop + radius)}px`;
circle.classList.add("ripple");
在将跨度元素添加到 DOM 之前,最好检查是否存在可能来自先前点击的任何剩余涟漪,并在执行下一个涟漪之前将其删除。
const ripple = button.getElementsByClassName("ripple")[0];
if (ripple) {
ripple.remove();
}
作为最后一步,我们将跨度作为子元素附加到按钮元素,以便将其注入按钮内部。
button.appendChild(circle);
我们的函数完成后,剩下的就是调用它。这可以通过多种方式完成。如果我们想将涟漪添加到页面上的每个按钮,我们可以使用类似以下内容
const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
button.addEventListener("click", createRipple);
}
现在我们有了工作的涟漪效果!
更进一步
如果我们想更进一步,并将此效果与对按钮位置或大小的其他更改结合起来会怎样?毕竟,能够自定义是我们选择自己重新创建效果的主要优势之一。为了测试扩展我们的函数有多容易,我决定添加一个“磁铁”效果,当光标位于某个区域内时,它会导致我们的按钮向光标移动。
我们需要依赖涟漪函数中定义的一些相同变量。为了避免不必要地重复代码,我们应该将它们存储在某个可供两种方法访问的地方。但我们也应该将共享变量的作用域限制在每个单独的按钮中。实现此目标的一种方法是使用类,如下例所示
由于磁铁效果需要在每次移动时跟踪光标,因此我们不再需要计算光标位置来创建涟漪。相反,我们可以依赖 cursorX
和 cursorY
。
两个重要的新变量是 magneticPullX
和 magneticPullY
。它们控制磁铁方法在光标之后拉动按钮的强度。因此,当我们定义涟漪的中心时,我们需要调整新按钮的位置(x
和 y
)和磁性拉力。
const offsetLeft = this.left + this.x * this.magneticPullX;
const offsetTop = this.top + this.y * this.magneticPullY;
要将这些组合效果应用于我们所有的按钮,我们需要为每个按钮实例化一个新的类实例
const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
new Button(button);
}
其他技术
当然,这只是实现涟漪效果的一种方法。在 CodePen 上,有很多示例展示了不同的实现方式。以下是我的一些最喜欢的示例。
仅 CSS
如果用户禁用了 JavaScript,我们的涟漪效果没有任何回退。但是,仅使用 CSS 可以接近原始效果,使用 :active 伪类来响应点击。主要限制是涟漪只能从一个位置出现——通常是按钮的中心——而不是响应我们的点击位置。Ben Szabo 的这个例子特别简洁
ES6 之前的 JavaScript
Leandro Parice 的演示类似于我们的实现,但它与早期版本的 JavaScript 兼容:
jQuery
此示例使用 jQuery 来实现涟漪效果。如果您已经将 jQuery 作为依赖项,它可以帮助您节省几行代码。
React
最后,我再举一个例子。虽然可以使用 React 功能(如状态和引用)来帮助创建涟漪效果,但这些并不是严格必要的。涟漪的位置和大小都需要为每次点击计算,因此将这些信息保存在状态中没有任何优势。此外,我们可以从点击事件中访问按钮元素,因此我们也不需要引用。
此 React 示例使用与本文首次实现相同的 createRipple
函数。主要区别在于——作为 Button
组件的方法——我们的函数的作用域限于该组件。此外,onClick
事件侦听器现在是我们 JSX 的一部分
太棒了…!!
实际 Material Design 水波纹效果的开发人员之一几年前写了一篇文章,介绍了不同实现和动画方式的权衡。该链接将是本文的一个不错的补充。
太酷了!感谢分享!
不错!!我也想看看 Material Tooltips 的教程!(比如 Google Docs 或 YouTube 中使用的那些),它们很酷!
我修改了该 Pen 以创建 Houdini 版本。目前这确实将支持限制为 Chromium 浏览器,但它也允许我们无需额外的元素或伪元素即可获得效果,因为水波纹现在是一个
radial-gradient()
,并且它还简化了 JavaScript,因为我们不需要计算水波纹相对于按钮的大小。工作原理
x, y
(设置为自定义属性)处的一个径向渐变,它填充具有可变 alpha(设置为自定义属性--a
)的半透明白色,直至可变半径(设置为自定义属性--r
)--a
和--r
自定义属性,以便在必要时对其进行动画处理.ani
类(最初没有),那么我们将这些自定义属性分别从.7
动画到0
和从0%
到100%
--x
和--y
变量设置为相对于按钮左上角的单击点的坐标.ani
类(这将启动 CSS 动画).ani
类这是一种非常有趣的方法。感谢您的分享,并感谢您抽出时间解释最重要的更改!
不错,但是对于输入而不是按钮呢?
不错的技巧,我将其移植到 Svelte这里。我更改了一些内容,使其在外观上更接近官方的 Material 实现(向水波纹 span 添加模糊、降低初始不透明度和更大的初始缩放比例)。
我认为这里唯一缺少的是,在移动设备上 clientX 和 Y 不存在,您需要使用 pageX 和 Y,这意味着首先要检查触摸事件,无论如何,这项工作都非常棒。
您好 JHeth 等人.. 我将您的示例转换为 Svelte 操作,该操作允许将水波纹效果应用于任何内容,同时还使其可以通过 CSS、CSS 属性和常规属性高度可配置。
https://svelte.net.cn/repl/4430fde7d42a4b03845d81411e701702?version=3.44.3
感谢这篇文章!
我做的一件事与众不同,而不是在下次点击时手动删除现有水波纹,您还可以监听动画结束并在之后直接删除水波纹
circle.addEventListener('animationend', () => button.removeChild(circle))
这里不错,感谢分享
很棒的文章!这与我为 Vue 创建 v-wave 指令的方式非常相似。主要区别在于水波纹的行为更接近 Android 上的实现,因为它出现在
pointerdown
上(除非触发了pointercancel
)并且保持可见,直到触发pointerup
事件。很棒的工作!对于任何尝试将其应用于绝对定位元素的人,我发现使用
event.offsetX
和event.offsetY
作为更可预测的代码模式。基本上将
circle.style.[top|left]
计算更改为如果您的按钮的 offsetParent 不是 body(锚定在 top/left===0 处),则可能只需要进行此更改。
将
pointer-events: none;
添加到span.ripple
样式中,以避免在非常快速地单击时(即在水波纹上)错误计算 offsetX 和 offsetY。非常感谢您的分享。
event.clientX
相对于视口,而 button.offsetLeft 相对于 button.offsetParent。示例仅按文章中所述工作,因为“button”是 body 的直接子元素。因此,我们可能会使用button.getBoundingClientRect()
来检索按钮的 left 和 top 属性,它们相对于视口。然后代码可以改写为另一个问题是在每次调用
createRipple()
时删除和添加 span 元素。创建 span 元素后,代码应通过类切换动画。用 SVG 六边形替换圆形,添加一点 CSS 旋转,瞧!
旋转的六边形点击点显示
现在我们也可以摇晃更大的空间以及按钮了。
糟糕,链接失败了
在 VueJS 中使用 pageX pageY 而不是 clientX clientY
如果我可以指出,只有在将
clientX
和clientY
更改为pageX
和pageY
后,我才能使 React 示例正常工作。有人可以解释一下 left 和 top 的计算方法吗?
circle.style.left =
${event.clientX - (button.offsetLeft + radius)}px
;circle.style.top =
${event.clientY - (button.offsetTop + radius)}px
;所以我在我的网站上尝试了这个,但意识到它在滚动时效果不佳。我可以通过在按钮前放置许多
<br>
来确认这一点。按钮向下移动,但水波纹仍将固定在其原始位置。因此,按钮没有水波纹。有没有人有针对此问题的仅 CSS 或 HTML 解决方案?我有一个 JS 问题,它通过 window.scrollY 值抵消了水波纹的 y 轴。我真的很感激一些帮助。
您必须考虑 window.scrollX 和 window.scrollY。工作演示https://codepen.io/giuliano-oliveira/pen/YzNYJZY
当按钮的父元素不是 body 时,对于
element.offset
,我们需要类似这样的内容const getOffsetTop = element => {
。let offsetTop = 0;
while (element) {
offsetTop += element.offsetTop;
element = element.offsetParent;
}
return offsetTop;
};
https://medium.com/@alexcambose/js-offsettop-property-is-not-great-and-here-is-why-b79842ef7582
感谢本教程!我正在白色背景上使用水波纹。
linear
过渡对我来说看起来有点奇怪。水波纹在某一刻出现,然后突然消失。所以我更倾向于使用
ease-out
而不是linear
。(主要用于background
属性,但即使它也影响transform
,看起来也不错,尽管现实世界中的涟漪不会减速。)友善提醒,此演示代码无法通过WCAG
https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html
在键盘焦点上使用
outline:0
且没有其他可见变化会导致此操作无法满足成功标准。您好。对于那些希望将文章中的代码片段用于自己项目的人,请合并上面发布的这两条评论
– 使用
offsetX
和offsetY
(由Angelo Gonzales提供),以及– 动画结束后移除涟漪(由Jonas提供)。
我最初只是简单地复制粘贴文章中的代码,结果却发现了一个错误。不要重蹈我的覆辙。
如果使用元素的
offset
来获取其位置,当在相对父元素中使用时会遇到问题。与其使用offset
,不如使用getBoundingClientRect()
。效果很好! :)