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 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.
// 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.