An in-depth tutorial on how to build the ripple effect outlined under Google Material Design's Radial Action specification and combine it with the powers of SVG and GreenSock.

With the advent of Google’s Material Design came a visual language that set out to create a unified experience across platforms and devices. Google’s examples depicted through their Animation section of the Material Guidelines has become so identifiable in the wild that many have come to know these interactions as part of the Google brand.

In this tutorial we’ll show you one way of building the ripple effect specifically outlined under Radial Action of the Google Material Design specification by combining it with the powers of SVG and GreenSock.

Responsive Action

Google defines Responsive Interaction using Radial Action as follows:

Radial action is the visual ripple of ink spreading outward from the point of input. The connection between an input event and on-screen action should be visually represented to tie them together. For touch or mouse, this occurs at the point of contact. A touch ripple indicates where and when a touch occurs and acknowledges that the touch input was received. Transitions, or actions triggered by input events, should visually connect to input events. Ripple reactions near the epicenter occur sooner than reactions further away.

Google makes it very clear that input feedback should take place from it’s origin and spread outwards. For example, if a user clicks a button directly in the center, this ripple will expand outward from that point of initial contact. This is how we indicate where and when a touch occurs in order to acknowledge to the user that input was received.

Radial Action In SVG

The ripple technique has been authored by many developers using primarily CSS techniques such as @keyframes , transitions , transforms pseudo trickery, border-radius and even extra markup such as a span or div . Instead of using CSS, let’s take a look at how SVG can be used to create this radial action using GreenSock’s TweenMax library for the motion.

Creating The SVG

Believe it or not you don’t need a fancy application like Adobe Illustrator or even Sketch to author this effect. The markup for the SVG can be written using a few XML tags that might already be familiar and in use with your own work.

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <symbol viewBox="0 0 100 100"></symbol> </svg>

For those using SVG sprites for icons, you’ll notice the use of <symbol> . The symbol element allows authors to match the correlating XML within individual symbol instances and subsequently instantiate them—or in other words—use them across an application like a stamp. Each instance stamped is identical to it’s sole creator; the symbol it resides within.

Symbol elements accept attributes such as viewBox and preserveAspectRatio that provide a symbol the scale-to-fit ability within a rectangular viewport defined by the referencing use element. Sara Soueidan wrote a wonderful article and built an interactive tool to help understand the viewBox coordinate system once and for all. Simply put, we’re defining the initial x and y coordinate values (0,0) and finally defining a width and height (100,100) of the SVG canvas.

The next piece to this XML puzzle is adding a shape we intend to animate as the ripple. This is where the circle element comes in.

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <symbol viewBox="0 0 100 100"> <circle /> </symbol> </svg>

The circle will need a bit more information then what it posseses in order to display correctly within the SVG’s viewBox .

<circle cx="1" cy="1" r="1"/>

The attributes cx and cy are coordinate positions relative to the viewBox of the SVG; symbol in our case. In order to make the click feel natural, we’ll need to make sure the trigger point rests directly underneath the user’s finger tip when input is received.

The attributes for the middle example of this diagram create a 2px x 2px circle with a radius of 1px. This will ensure our circle doesn’t crop like we see in the bottom example of the diagram.

<div style="height: 0; width: 0; position: absolute; visibility: hidden;" aria-hidden="true"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false"> <symbol id="ripply-scott" viewBox="0 0 100 100"> <circle id="ripple-shape" cx="1" cy="1" r="1"/> </symbol> </svg> </div>

For the final touches, we’ll wrap it with a div containing inline CSS for brevity to hide the sprite. This prevents it from taking up space in the page when rendered.

As of this writing, an SVG sprite containing symbol blocks that reference it’s own gradient definition—as you’ll see in the demos— by ID cannot find the gradient and render it properly; the reason for the visibility property used in place of display: none as the entire gradient fails on Firefox and most other browsers.

The use of focusable="false" is required for all IE’s up to 11; with the exception of Edge as it has yet to be tested. It was a proposal from the SVG 1.2 specification describing how keyboard focus control should work. IE implemented this, but no one else did. For consistency with HTML, and also for greater control, SVG 2 is adopting tabindex instead. HT to Amelia Bellamy-Royds for the schooling on this tip.

Making Markup

Writing solid markup is the reason we all get up in the morning so let’s write a semantic button element to use as our object that will reveal this ripple.

<button>Click for Ripple</button>

The markup structure for the button that most are familiar with is straight forward including some filler text.

<button> Click for Ripple <svg> <use xlink:href="#ripply-scott"></use> </svg> </button>

To take advantage of the symbol element created earlier, we’ll need a way to reference it by utilizing the use element inside the button’s SVG referencing the symbol’s ID attribute value.

<button id="js-ripple-btn" class="button styl-material"> Click for Ripple <svg class="ripple-obj" id="js-ripple"> <use width="100" height="100" xlink:href="#ripply-scott" class="js-ripple"></use> </svg> </button>

The final markup possesses additional attributes for CSS and JavaScript hooks. Attribute values beginning with “js-” denote values only present in JavaScript and thereby removing them would hinder interaction, but not affect styling. This helps differentiate CSS selectors from JavaScript hooks in order to avoid one another from causing confusion when removal or updating is required in the future.

The use element must have a width and height defined otherwise it will not be visible to the viewer. You could also define this in CSS if you decided against having it directly on the element itself.

Styling The Joint

When it comes time for authoring the CSS, very little is required to achieve the desired result.

.ripple-obj { height: 100%; pointer-events: none; position: absolute; top: 0; left: 0; width: 100%; z-index: 0; fill: #0c7cd5; } .ripple-obj use { opacity: 0; }

Here’s what remains when removing the declarations used for general styling. The use of pointer-events eliminates the SVG ripple from becoming the target of mouse events as we only need the parent object to react; the button element.

The ripple must be invisible initially hence the opacity value set to zero. We’re also positioning the ripple object in the top left of the button . We could center the ripple shape, but since this event occurs based on user interaction it’s meaningless to fret over position.

Giving It Life

Breathing life into this interaction is what it’s all about and exactly what Material Design guidelines document as one of the most crucial parts of their visual language.

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"></script> <script src="js/ripple.js"></script>

To animate the ripple we’ll be using GreenSock’s TweenMax library because it’s one of the best libraries out there to animate objects using JavaScript; especially when it comes to the headaches involved with animating SVG cross-browser.

var ripplyScott = (function() {} return { init: function() {} }; })();

The pattern we’re going to be using is what’s called a module pattern as it helps to conceal and protect the global namespace.

var ripplyScott = (function() {} var circle = document.getElementById('js-ripple'), ripple = document.querySelectorAll('.js-ripple'); function rippleAnimation(event, timing) {…} })();

To kick things off we’ll grab a few elements and store them in variables; particularly the use element and it’s containing svg within button . The entire animation logic will reside within the rippleAnimation function. This function will accept arguments for timing of the animation sequence and event information.

var ripplyScott = (function() {} var circle = document.getElementById('js-ripple'), ripple = document.querySelectorAll('.js-ripple'); function rippleAnimation(event, timing) { var tl = new TimelineMax(); x = event.offsetX, y = event.offsetY, w = event.target.offsetWidth, h = event.target.offsetHeight, offsetX = Math.abs( (w / 2) - x ), offsetY = Math.abs( (h / 2) - y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) + offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); } })();

A ton of variables have been defined so let’s hit them one by one to discuss what they’re in charge of.

var tl = new TimelineMax();

This variable creates the timeline instance for the animation sequence and the way all timelines are instantiated in TweenMax.

var x = event.offsetX; var y = event.offsetY;

An event’s offset is a read-only property that reports the offset value from the mouse pointer to the padding edge of the target node. In this case that would be our button . The event offset is calculated from left to right for x and top to bottom for y; both starting at zero.

var w = event.target.offsetWidth; var h = event.target.offsetHeight;

These variables are returning the width and height of the button. Final calculations will include the size of the element’s border’s and padding. We’ll need this value to know how large our element is so we can spread the ripple to the farthest edge.

var offsetX = Math.abs( (w / 2) - x ); var offsetY = Math.abs( (h / 2) - y );

The offset values are the click’s offset distance away from the element’s center. In order to fill the entire area of our target, the ripple must be large enough to cover from the point of contact to the farthest corner. Using our initial x and y coordinates won’t cut it as once again the values start from zero going from left to right for x and top to bottom for y. This approach lets us use those values, but detects the distance no matter what side is clicked of the target’s central point.

Notice how the circle will cover the entire element each click no matter where the initial point of input takes place. To cover the entire surface according to the initiated point of interaction we need to do some maths.

Here’s how the offset is calculated using 464 x 82 as our width and height, 391 and 45 as our x and y coordinates:

var offsetX = (464 / 2) - 391 = -159 var offsetY = (82 / 2) - 45 = -4

We find the center by dividing the width and height in half then subtract the values reported detected by our x and y coordinates.

The Math.abs() method returns the absolute value of a number. Using our arithmetic above that would make the values 159 and 4.

var deltaX = 232 + 159 = 391; var deltaY = 41 + 4 = 45;

Delta calculates the entire distance of our click instead of the distance to the center. The reason for delta is that x and y always start at zero going from left to right so we need a way to detect the click when it’s in the opposite direction; right to left.

For those that participated in basic math courses throughout highschool will recognize the Pythagorean Theorem. This formula takes the altitude squared(a) plus the base squared(b) and results in the hypotenuse squared(c).

a2 + b2 = c2

var scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

Using this formula let’s run through the calculations.

var scale_ratio = Math.sqrt(Math.pow(391, 2) + Math.pow(45, 2));

The method Math.pow() returns the power of the first argument; in this case doubled. The value 391 to the power of 2 would be 152881. The last value of 45 to power of 2 equals 2025. Adding both these values and taking the square root of the result would leave us with 393.58099547615353 which is the ratio we need to scale the ripple by.

var ripplyScott = (function() { var circle = document.getElementById('js-ripple'), ripple = document.querySelectorAll('.js-ripple'); function rippleAnimation(event, timing) { var tl = new TimelineMax(); x = event.offsetX, y = event.offsetY, w = event.target.offsetWidth, h = event.target.offsetHeight, offsetX = Math.abs( (w / 2) - x ), offsetY = Math.abs( (h / 2) - y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) + offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); tl.fromTo(ripple, timing, { x: x, y: y, transformOrigin: '50% 50%', scale: 0, opacity: 1, ease: Linear.easeIn },{ scale: scale_ratio, opacity: 0 }); return tl; } })();

Using the fromTo method in TweenMax we’re passing the target—ripple shape—and setting up object literals that contain our directions for the entire motion sequence. Seeing as we’d like to animate form the center outwards, the SVG needs the transform-origin set to the middle position. The scaling also needs to be adjusted to it’s smallest position, given an ease for feeling and setting opacity to 1 since we’d like to animate in then out. If you recall earlier we set the use element with an opacity of 0 in CSS and the reason why we’d like to go from a value of 1 and return to zero. The final part is returning our timeline instance.

var ripplyScott = (function() { var circle = document.getElementById('js-ripple'), ripple = document.querySelectorAll('.js-ripple'); function rippleAnimation(event, timing) { var tl = new TimelineMax(); x = event.offsetX, y = event.offsetY, w = event.target.offsetWidth, h = event.target.offsetHeight, offsetX = Math.abs( (w / 2) - x ), offsetY = Math.abs( (h / 2) - y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) + offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); tl.fromTo(ripple, timing, { x: x, y: y, transformOrigin: '50% 50%', scale: 0, opacity: 1, ease: Linear.easeIn },{ scale: scale_ratio, opacity: 0 }); return tl; } return { init: function(target, timing) { var button = document.getElementById(target); button.addEventListener('click', function(event) { rippleAnimation.call(this, event, timing); }); } }; })();

This object literal returned will control our ripple by attaching an event listener to the desired target, calling upon our rippleAnimation and finally passing arguments we’ll discuss in our next step.

ripplyScott.init('js-ripple-btn', 0.75);

The final call is made to our button by using our module and passing the init function that passes our button and the timing for the sequence. Voilà!

We hope you enjoy this little experiment and find it inspiring! Don’t forget to check out the demos with the different shapes and take a look at the source code. Try new shapes, layer shapes, but most importantly stay creative.

Attention: Some of these techniques are very experimental and will only work in modern browsers.