Let's build Vue directives: Adding a wave effect to buttons
Tags: vue
Published: 2021-12-10 15:23:46

> TL: DR - take me to the code  
>   
> 1\. [Vue 2 Code Sandbox](https://codesandbox.io/s/v-ripple-vue-2-wonkg)  
> 2\. [Vue 3 Code Sandbox](https://codesandbox.io/s/v-ripple-vue-3-joji0)  
> 3\. [Difference between Vue 2 and Vue 3 implementation](https://v3.vuejs.org/guide/migration/custom-directives.html#overview.)

Material design was built around the idea of creating clean user interfaces with rich user feedback. One part of its toolkit is the '[Ripple Component](https://material.io/develop/ios/supporting/ripple)'. Whenever an element that uses 'Ripple' is clicked, it emits waves outwards from the mouse pointer. This signals a user that the click, or touch respectively, was recognized.

Using it in your Vue.js web application provides you this simple, yet elegant way of responding to user interactions.

Reasons for a Vue directive
---------------------------

There are several excellent guides on the internet on how to achieve the same functionality with vanilla Javascript. There are also pure CSS implementations. Bear with me though, a custom Vue directive is still a valid choice, because it:

*   is easier to reuse - all styling and animation happens inside the directive's code
*   requires no selectors but uses Vue's built-in low-level DOM access
*   can be directly attached to any element with `v-ripple`

> Please note that this is not an exact replica of the Mateiral Design implementation style. You can, however, tweak the directive's functionality according to your needs

Setup a Vue 3 Project with Vite
-------------------------------

We'll use [Vite](https://vitejs.dev/guide/) to spin up a basic application. You can alternatively use [Vue CLI](https://cli.vuejs.org/guide/).

Change into a directory of your choice and type:

```shell
# 1: Init your project
npm init vite@latest # using Vite with npm
# yarn create vite   # using Vite with yarn
# vue create .       # using Vue CLI

# 2: Change into the created folder and start the dev server
cd vite-project
npm install
npm run dev
```

Make the app object available for directive registration
--------------------------------------------------------

Before registering our custom directives, let's make a small adjustment in Vue's main file. It uses `createApp` on the fly, but we need the created app object to register components on.

> This step is optional, you could also chain `.directive()` as part of the createApp bootstrapping process.

```js
// Inside main.js: Change this 
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

// ------

// to 
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// ... register directives here

app.mount('#app')
```

With `app.directive(directiveName, directiveFunction)`, we're now able to register our own directives everywhere in the app.

Create the ripple directive
---------------------------

The functionality we are about to achieve breaks down into three significant components:

*   A parent function is responsible to create a DOM helper element and handling the following two commands
*   One nested function applies styles to the helper element
*   A second nested function creates the ripple animation layout

We can then use the standard Vue 3 `mounted` lifecycle to apply the directive. Let's start by creating a `/directives` folder in the `/src` of our project and name it `vRipple.js`.

### The parent function

In this file, add the following code

```js
const handleRipple = (element, binding, ev) => {
  const rippleElement = document.createElement("span");
  let currentDiameter = 1;
  let currentOpacity = 0.65;
  let animationHandler = setInterval(animateRippleSpread, 15);
  applyRippleStyle();

  /* function applyRippleStyle() {...} */

  /* function animateRippleSpread() {...} */
};

// Hook the directive to the DOM element
const vRipple = {
  mounted: (el, binding) => {
    el.style.position = "relative";
    el.style.overflow = "hidden";
    el.addEventListener("click", (ev) => handleRipple(el, binding, ev));
  }
};

export default vRipple;
```

We will use `currentDiameter` and `currentOpacity` for the wave effect. An interval handler will come in handy to halt the animation once its spread reaches the outer limits.

### Apply the basic ripple effect style

The first child function needs to be called once the `span` helper element is created. It calculates where on the button the click took place and handles positioning and basic styles accordingly. You can change these to match your own taste or even extend them.

```js
function applyRippleStyle() {
  const elementCoordinates = element.getBoundingClientRect();
  const offsetY = ev.clientY - elementCoordinates.y;
  const offsetX = ev.clientX - elementCoordinates.x;

  rippleElement.style.position = "absolute";
  rippleElement.style.height = "5px";
  rippleElement.style.width = "5px";
  rippleElement.style.borderRadius = "100%";
  rippleElement.style.backgroundColor = "#f2f2f2";
  rippleElement.style.left = `${offsetX}px`;
  rippleElement.style.top = `${offsetY}px`;
  ev.target.appendChild(rippleElement);
}
```

### Create the ripple animation

Inside `animateRippleSpread`, we're letting the actual magic happen. This function is called every 15 milliseconds. It conditionally alters the size and opacity of the `span` helper **or** removes the element once its maximum diameter is reached.

```js
function animateRippleSpread() {
  const maximalDiameter = +binding.value || 50;
  if (currentDiameter <= maximalDiameter) {
    currentDiameter++;
    currentOpacity -= 0.65 / maximalDiameter;
    rippleElement.style.transform = `scale(${currentDiameter})`;
    rippleElement.style.opacity = `${currentOpacity}`;
  } else {
    rippleElement.remove();
    clearInterval(animationHandler);
  }
}
```

> Note that when you bind a value to the directive, the animation's duration will increase with the maximum size of the ripple element. This will result in a longer and larger wave.

We're almost done. The one thing left to do is to register the directive and try it out:

Inside the `main.js` file, register the directive as follows:

```js
import { createApp } from 'vue'
import App from './App.vue'
import vRipple from './directives/vRipple'

const app = createApp(App)

app.directive('ripple', vRipple)

app.mount('#app')
```

Use the directive on the template
---------------------------------

All left to do is to apply `v-ripple` to an element of your choice. You can either try this out in your own environment or interactively using the Code Sandboxes for Vue2 or [Vue3](https://codesandbox.io/s/v-ripple-vue-3-joji0?file=/src/App.vue).

And there we have it. A fully functional ripple directive that provides rich user feedback upon clicking an element.