Back

Let's build Vue directives: Adding a wave effect to buttons

TL: DR - take me to the code

1. Vue 2 Code Sandbox
2. Vue 3 Code Sandbox
3. Difference between Vue 2 and Vue 3 implementation

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'. 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 to spin up a basic application. You can alternatively use Vue CLI.

Change into a directory of your choice and type:

# 1: Init your project
npm init [email protected] # 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.
// 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

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.

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.

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:

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.

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


Read more about vue_directives