I recently looked for ripple effect plugin/library but couldn’t get one that is simple and easy enough for me. So I do what I have to do: learn how it works and craft it myself.

What a ripple effect is

There’s might be earlier use of ripple effect, but I personally know about it after the introduction of Material Design by Google. Ripple effect is basically a rippling element which appears after user clicks a link.

This is how normal link appears:


With ripple effect, this is how it appears:


How ripple effect works

There might be different interpretation on how it works, but I personally see it this way:

  1. A link (or any other element) is clicked
  2. Capture the offset of the clicked element and the offset of the click
  3. Create an overflow: hidden wrapper which mimics the dimension of clicked element. Place it using position: absolute relative to the body.
  4. Create a circular shape inside the wrapper. Animate it using CSS3’s transform: scale()
  5. Remove the created effect after proper amount of time

Let the code begins

Now that the logic of the effect has been understood, let’s find a way to do it. In my case, jQuery and CSS3 are the tool for the job:

1. Get the click event

Capturing click event can be done easily using .click() or .on(). I prefer .on() so it can capture DOM that is created on the fly. Let’s assume that the ripple effect will be implemented to any element which has .ripple-effect class.

$('body').on( 'click', '.ripple-effect', function(e){
    // The rest of the js code below will be placed here

2. Get the offsets of click event and clicked element

jQuery’s .offset() captures the top and left position of the clicked element relative to the document. jQuery’s event.pageX and event.pageY are used to determine the position of the click relative to the document.

// Cache the selector
var the_dom = $(this);

// Sometimes the clicked element != the limit of the ripple effect. We'll talk about it later below
var the_dom_limit = the_dom;

// Get the click and the clicked element offsets
var the_dom_offset = the_dom_limit.offset();		
var click_x = e.pageX;
var click_y = e.pageY;

3. Create an overflow: hidden wrapper which mimics the dimension of the clicked element

There are two ways of doing this:

  1. The wrapper is appended on body and positioned using using absolute positioning relative to the body
  2. The wrapper is appended inside the the clicked element

Both ways has their own flaw:

  1. Absolute positioning relative to the body is often misplaced if the clicked element is inside a fixed positioning element
  2. Wrapper cannot be placed inside self enclosing markup such as input, img, etc. We can wrap the clicked element instead of appending the wrapper inside the clicked element, but it’d be misplaced if the clicked element is positioned using absolute positioning.

For the time being, l’ll use the first option. That’s how some of jQuery UI modules such as timepicker are positioned, apparently.

// Get the width and the height of clicked element
var the_dom_width = the_dom_limit.outerWidth();
var the_dom_height = the_dom_limit.outerHeight();

// Draw the ripple effect wrap
var ripple_effect_wrap = $('<span class="ripple-effect-wrap"></span>');
	'width' 		: the_dom_width,
	'height'		: the_dom_height,
	'position' 		: 'absolute',
	'top'			: the_dom_offset.top,
	'left'	 		: the_dom_offset.left,
	'z-index' 		: 100,
	'overflow' 		: 'hidden',
	'background-clip'	: 'padding-box'

// Append the ripple effect wrap

4. Create a circular shape in the position of click area inside the wrapper made on point no.3, then scale it using CSS3

The circular shape uses absolute positioning relative to the ripple effect. To do so, the click position which is relative to the body should be converted into click position relative to the clicked element.

The circular shape is later scaled using CSS3’s scale().

One thing that I learned the hard way: the circular shape should have wide and tall dimension (1000px X 1000px would be fine) then scaled down when the animation starts then scaled up back to the original dimension. This will prevent the shape from being pixelated.

// Determine the position of the click relative to the clicked element
var click_x_ripple = click_x - the_dom_offset.left;
var click_y_ripple = click_y - the_dom_offset.top;
var circular_width = 1000;

// Draw the ripple effect
var ripple = $('<span class="ripple"></span>');
	'width' 			: circular_width,
	'height'			: circular_width,
	'background'			: 'rgba(0,0,0,0.3)',
	'position'			: 'absolute',
	'top'				: click_y_ripple - ( circular_width / 2 ),
	'left'				: click_x_ripple - ( circular_width / 2 ),
	'content'			: '',
	'background-clip' 		: 'padding-box',
	'-webkit-border-radius'     	: '50%',
	'border-radius'             	: '50%',
	'-webkit-animation-name'	: 'ripple-animation',
	'animation-name'              	: 'ripple-animation',
	'-webkit-animation-duration'  	: '2s',
	'animation-duration'          	: '2s',
	'-webkit-animation-fill-mode' 	: 'both',
	'animation-fill-mode'         	: 'both'  			
$('.ripple-effect-wrap:last').append( ripple );

Register an animation called ripple-animation on the CSS file:

@-webkit-keyframes ripple-animation {
  0% {
    -webkit-transform: scale(.01);
            transform: scale(.01);

  100% {
    -webkit-transform: scale(5);
            transform: scale(5);

@keyframes ripple-animation {
  0% {
    -webkit-transform: scale(.01);
            transform: scale(.01);

  100% {
    -webkit-transform: scale(5);
            transform: scale(5);

5. After proper amount of time, remove the effect’s DOM

After the animation is presented, remove the DOM. Clean as you go.

// Remove rippling component after half second
setTimeout( function(){
}, 500 );

The code in action

Let’s put the code above on jsFiddle to see how it works:

Looks pretty neat, no?

The known caveats

The naughty Safari

The code above work flawlessly in the recent version of Chrome and Firefox. However, it doesn’t work on Safari and mobile Safari. I don’t know the exact causes, but based on my observation, Safari didn’t execute any effect inside the .click() after element clicking if the default behavior isn’t prevented.

My current workaround for this are:

  1. Prevent the default behavior using e.preventDefault() on the beginning of the click event
  2. Detect the href of the clicked element. If it points to new URL, redirect the window. If it is a hash which doesn’t point to new URL, let it be.

Here’s how it is coded:

// Get the href
var href = the_dom.attr(‘href’);

// Safari appears to ignore all the effect if the clicked link is not prevented using preventDefault()
// To overcome this, preventDefault any clicked link
// If this isn’t hash, redirect to the given link
if( typeof href != ‘undefined’ && href.substring(0, 1) != ‘#’ ){
    setTimeout( function(){
        window.location = href;
    }, 200 );

Consequence of The naughty Safari fix for input

If the effect is implemented to button / input submit, the fix for naughty safari above will fail all the form submission. Thus, detect the clicked DOM and do this ugly hack of removing the effect class (and add it again later) and manually trigger the button

// Ugly manual hack to fix input issue
if( the_dom.is(‘input’) || the_dom.is(‘button’) ){
    setTimeout( function(){
    }, 200 );

The Possible Improvement

The code above works well so far. However, there’s a room for improvement tho:

  1. Making the ripple limit customizable
  2. Making ripple color customizable
  3. Making ripple’s radius size customizable
  4. Enabling user to add custom class for further customization

Flexible ripple wrap limit

The code above assume that the size of the ripple wrap mimics the size of the clicked element. However, there’s a case where you want to use the parent of the clicked element. For example: the clicked element is .widget > li > a but the ripple covers .widget > li.

To do so, we can utilize HTML5’s custom data- attribute. How it can be done:

  1. Use custom data- attribute for defining ripple effect limit. Let’s use data-ripple-limit.
  2. If data-ripple-limit doesn’t exist, the dimension for the ripple effect is the clicked element.
  3. If data-ripple-limit exists, the dimension for the ripple effect is the selector provided in data-ripple-limit

Flexible color

Sometimes rgba( 0, 0, 0, 0.3 ) isn’t the color you want for the ripple effect. Similar to the ripple wrap limit customization, custom data- attribute can be utilized for similar manner. How it can be done:

  1. Use custom data- attribute for defining ripple color is data-ripple-color.
  2. If data-ripple-color doesn’t exist, the color for the ripple effect is rgba( 0, 0, 0, 0.3 ).
  3. If data-ripple-color exists, use this color instead

Flexible radius

Sometimes the clicked element isn’t a perfect rectangular shape. There’s a solid chance that the clicked element has border-radius. To anticipate this, custom data- attribute can be utilized too. Let’s use data-ripple-wrap-radius for this.

The simple if else mechanism similar to two points above can be used.

Custom class for ripple wrap

If in some case you need more than equal radius on four corner, you might want to add custom css to the ripple’s wrap then modify the ripple wrap or the ripple effect through custom css.

The simple if else mechanism similar to two points above can be used.

Code + known fix + improvement

Here’s how the code + known fix + improvements above looks like: Simple Ripple Effect Demo

Interested in using this ripple effect on your project? You can download or fork this GitHub repo.

3 thoughts on “How To Create Ripple Effect Using jQuery & CSS3

  1. Not working @ Safari (3. Flexible Radius)
    – when click ripple go out of radius
    – please help any idea to fix it. Only this issue, else working fine love it. Waiting for your reply
    -Thanks! for all your good tutorial

    1. 3. Flexible Radius in Demo page
      here: http://fikrirasy.id/demo/simple-ripple-effect

  2. Hi,
    On iPad, it does’nt work if there is nothing inside the div. If there is an image, it works if I click on the image, but not next to it.

    This only happens on my iPad. Any idea why?


Share Your Thought