v-ripple 涟漪
v-ripple 是一条全局指令,不是组件。把它挂在任意元素(原生 div / button 或别的组件)上,按下时就会出现 Material 风格的扩散波纹。它复刻了 MUI TouchRipple 的动效细节:按住常显、松手淡出、触摸延迟出现、键盘聚焦时从中心脉冲。库里的 MButton、MFab、MCheckbox 等组件已经在内部挂好了这条指令。
代码演示
基础用法
不带值即为默认参数,指令会自动给静态定位的宿主补上 position: relative。
按住不放可以保持波纹;松开或把指针移出元素后波纹淡出。键盘 Tab 聚焦后按 Enter / 空格也会出现居中的脉冲波纹。
<script setup lang="ts">
import type { CSSProperties } from 'vue'
const filled: CSSProperties = {
padding: '12px 18px',
border: 0,
borderRadius: '8px',
background: 'var(--m-primary)',
color: 'var(--m-primary-contrast)',
font: 'inherit',
cursor: 'pointer',
userSelect: 'none',
}
const outlined: CSSProperties = {
padding: '11px 16px',
border: '1px solid var(--m-border)',
borderRadius: '8px',
background: 'var(--m-card)',
color: 'var(--m-text-default)',
font: 'inherit',
cursor: 'pointer',
userSelect: 'none',
}
</script>
<template>
<div class="m-row-demo">
<!-- 普通 div:指令会自动补上 position: relative -->
<div v-ripple :style="filled">按住看我</div>
<!-- 原生 button -->
<button v-ripple type="button" :style="outlined">button</button>
</div>
</template>禁用涟漪
三种关闭方式:绑定 false、传 disabled: true,或者宿主本身是原生 disabled / aria-disabled="true"。
第三颗按钮没有做任何配置,仅因为它是原生 disabled 按钮,波纹就不会出现。
<script setup lang="ts">
import type { CSSProperties } from 'vue'
const plain: CSSProperties = {
padding: '12px 18px',
border: '1px solid var(--m-border)',
borderRadius: '8px',
background: 'var(--m-card)',
color: 'var(--m-text-default)',
font: 'inherit',
cursor: 'pointer',
}
const nativeDisabled: CSSProperties = {
...plain,
background: 'var(--m-action-disabled-bg)',
color: 'var(--m-action-disabled)',
cursor: 'not-allowed',
}
</script>
<template>
<div class="m-row-demo">
<button v-ripple="false" type="button" :style="plain">v-ripple="false"</button>
<button v-ripple="{ disabled: true }" type="button" :style="plain">disabled: true</button>
<!-- 原生 disabled 由指令自身识别,无需额外配置 -->
<button v-ripple type="button" disabled :style="nativeDisabled">原生 disabled</button>
</div>
</template>自定义选项
color、duration、initialScale、center 四个选项可以组合使用。
duration 同时作用于进入与退出;initialScale: 0.5 让波纹从一半大小开始扩散,起点更明显、观感更快。
<script setup lang="ts">
import type { CSSProperties } from 'vue'
interface Host {
label: string
color?: string
duration?: number
initialScale?: number
center?: boolean
filled?: boolean
}
const base: CSSProperties = {
padding: '14px 20px',
borderRadius: '8px',
cursor: 'pointer',
userSelect: 'none',
}
const hosts: Host[] = [
{ label: 'color', color: '#ffffff', filled: true },
{ label: 'duration 200', duration: 200 },
{ label: 'initialScale 0.5', duration: 300, initialScale: 0.5 },
{ label: 'center', center: true },
]
function hostStyle(host: Host): CSSProperties {
return host.filled
? { ...base, background: 'var(--m-primary)', color: 'var(--m-primary-contrast)' }
: {
...base,
border: '1px solid var(--m-border)',
background: 'var(--m-card)',
color: 'var(--m-text-default)',
}
}
</script>
<template>
<div class="m-row-demo">
<div
v-for="host in hosts"
:key="host.label"
v-ripple="{
color: host.color,
duration: host.duration,
initialScale: host.initialScale,
center: host.center,
}"
:style="hostStyle(host)"
>
{{ host.label }}
</div>
</div>
</template>用在自定义元素与组件上
指令不挑宿主,普通 div 也能用;而已经自带涟漪的组件不需要再挂一次。
MButton 内部已经有 v-ripple,再挂一次不会出现第二层波纹(容器记录在同一个宿主元素上),但会多出一套事件监听,没有必要。
<script setup lang="ts">
import type { CSSProperties } from 'vue'
const custom: CSSProperties = {
padding: '10px 16px',
borderRadius: '8px',
background: 'var(--m-action-hover)',
color: 'var(--m-text-default)',
cursor: 'pointer',
userSelect: 'none',
}
</script>
<template>
<div class="m-row-demo">
<!-- MButton 内部已经挂了 v-ripple -->
<MButton variant="contained" color="primary">组件自带涟漪</MButton>
<!-- 再挂一次属于重复挂载,看不出额外效果,也会多出一套监听 -->
<MButton v-ripple variant="outlined" color="primary">重复挂载 v-ripple</MButton>
<!-- 指令也可以用在任意自定义元素上 -->
<div v-ripple :style="custom">自定义元素</div>
</div>
</template>API
绑定值 RippleOptions
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
disabled | boolean | undefined | 禁用涟漪 |
color | string | undefined | 涟漪颜色;缺省跟随宿主的 currentColor |
duration | number | 550 | 动画时长(ms),进入与退出共用 |
initialScale | number | 0 | 起始缩放,即从 initialScale 扩散到 1 |
center | boolean | false | 是否从元素中心扩散;false 时从点击点扩散 |
export type RippleOptions =
| boolean
| {
disabled?: boolean // 禁用涟漪
color?: string // 涟漪颜色,默认跟随 currentColor
duration?: number // 动画时长(ms),默认 550(进入与退出共用)
initialScale?: number // 起始缩放,默认 0(即从 0 扩散到 1)
center?: boolean // 是否从元素中心扩散,默认 false
}绑定值的几种写法:
| 写法 | 含义 |
|---|---|
v-ripple | 使用全部默认参数 |
v-ripple="false" | 关闭涟漪 |
v-ripple="{ duration: 300 }" | 传入配置对象;updated 钩子会同步最新值,不需要重新绑定监听器 |
修饰符
无。所有配置都通过绑定值对象传入(disabled、color、duration、initialScale、center),没有 .center 这类修饰符写法。
行为细节
- 定位:宿主元素不是
position: static时,指令会自动把它设为position: relative,保证涟漪容器以宿主为定位上下文。 - DOM 结构:指令会向宿主
appendChild一个<span class="m-ripple">容器,它成为宿主的最后一个子节点,波纹是这个容器的子节点。容器本身是绝对定位(inset: 0)且pointer-events: none,不占布局空间,但依赖:last-child之类的选择器时要留意这个多出来的子节点。 - 触发条件:宿主为原生
disabled或aria-disabled="true"时不触发;鼠标只响应主键(event.button === 0)。 - 指针行为:
pointerdown按下即出现,波纹直径恰好覆盖元素最远角;按住保持常显,pointerup/pointercancel/ 鼠标pointerleave后淡出。 - 触摸与笔:延迟 80ms 出现,用来区分「滚动」与「按下」;期间发生滚动或移动会取消这次波纹。
- 键盘:
:focus-visible或按 Enter / 空格激活时产生居中脉冲波纹,失焦后停止。 - 颜色:默认取宿主的
currentColor并以约 30% 不透明度呈现,所以深底浅字的按钮会自动得到浅色波纹。
使用建议
深色底上的波纹不用特意调色
波纹默认跟随 currentColor,而 currentColor 就是宿主文字的颜色。MButton 的 contained 变体把文字色设成了对比色,所以涟漪天然可见;只有当你想要一个和文字色不同的波纹时才需要传 color。
<div v-ripple="{ color: '#ffffff' }">深色底上的白色涟漪</div>给自定义元素补上交互语义
指令只负责视觉反馈,不会给 div 加 tabindex 或 role。如果宿主是可以点击的自定义元素,记得自己补 role="button" 与 tabindex="0",否则键盘用户既聚焦不到、也不会有键盘涟漪。
自带涟漪的组件不要再挂一次
MButton、MFab、MCheckbox、MRadio、MSwitch、MDropdownItem、MSelect 选项、MTabs 的关闭 / 新增按钮等都已内置这条指令,并且各自传了合适的配置(例如 MCheckbox 用 { center: true })。在它们身上再写一次 v-ripple 属于重复挂载:容器与状态都记录在同一个宿主元素上,后者会覆盖前者的记录,只是多出一个空的涟漪容器和一套多余的监听,不会有更好的效果。
别把涟漪当点击反馈的全部
涟漪只在指针 / 键盘交互时出现,并且会被 prefers-reduced-motion 关掉。真正的状态变化(加载、成功、失败)仍然要用 loading、toast 这类明确的反馈来表达。