DEV Community

Dhilip kumar
Dhilip kumar

Posted on

Ripple Effect in React

First things First. So happy my Previous Post was shared by @thepracticaldev in twitter.
Thanks a lot 😋 A great motivation for me😋
In case you missed it, go here

Ripple effect is becoming an interesting topic in recent days. Although there are libraries like material-ui which helps in achieving this behaviour, it is important for us to understand how we can achieve this on our own without depending on External libraries.

What is Ripple Effect?

What happens when we throw a stone into a pool? It creates disorderliness in the pool in a pattern[Ripple] originating from the Stone’s point of contact with water.

Similarly when the user taps or clicks on a Button, Icon, Image etc., a ripple starts at the point of interaction and flows throughout the entire component and disappears eventually.

How to Create a Ripple Effect using React Component?

First we will include the boiler plate code for Rendering a Button in React.

.btn {
  margin: 50px auto;
  border-radius: 25px;
  background-color: #5300e8;
  box-shadow: 0 2px 4px 0 #888888;
  display: inline-block;
  padding: 15px 50px;
  color: #ffffff;
}
.center {
  text-align: center;
}
class Ripple extends React.Component {
  initializeState = () => {
    return {
      spanStyles: {},
      count: 0
    }
  }
  state = this.initializeState();
  render() {
    const {children = null, classes="", onClickHandler=null} = this.props;
    return (
      <div className={classes} onClick={this.onClickHandler}>
        {children}
      </div>
    );
  }
}

const App = (
  <div className="center">
    <Ripple classes="btn">Click Me</Ripple>
  </div>
);

ReactDOM.render(App, document.getElementById("app"));

Explanation:

CssBoilerPlateRipple.css

Color and Button Alignments are done.

JsRippleBoilerPlate.js

  • initializeState() function which returns an object that will be set to a state for the first time and whenever reset is required.
  • Herecount refers to number of continuous clicks user made before the component could clean up the spans, once it is cleaned up the counter is reset to 0. For each click we might have to have new position based on the coordinate the user clicks so we will have a unique spanStyles value for each count.
  • JSXApp is rendered inside an Element with id ‘app’ .
  • App is a Constant which has a component declaration(Ripple) within itself.Here we pass classes, handlers and other properties that you wish to add to the actual element that Ripple Renders.
  • Ripple is a Stateful Component which returns the children by wrapping it inside a div. At the end of it we have the following button.

Ripple — Overview

Let us start with understanding how the Ripple JSX look like.

<div class="ripple">
  {props.children}
  <div class="rippleContainer">
    <span><span>
  </div>
</div>
  • {props.children} Render all the children that we pass inside<Ripple></Ripple> fromApp.
  • After rendering the children we have a div (rippleContainer) which is appended to the parent element.
  • Span is created as a child torippleContainer at the time when the user clicks on the button.
  • We add some css classes and animation (explained later) to this span so that we will get a Ripple Effect.
  • Finally we remove the<span> after the animation is done.

Let’s Ripple :)

Updating the Render Method of Ripple Component.

render() {
    const {children= null, classes = "", onClickHandler = null} = this.props;
    return (
        <div ref="targetElement" className={'ripple ' + classes} onClick={onClickHandler}>
            {children}
            <div className="rippleContainer" onMouseDown={this.showRipple} onMouseUp={this.callCleanUp(this.cleanUp, 2000)}>
                {this.renderRippleSpan()}
            </div>
        </div>
    );
}
.ripple {
  position: relative;
  overflow: hidden;
}
.ripple .rippleContainer {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}
  • We addripple class to the parent div and assign styling making it relative and hidden overflow.
  • rippleContainer child is appended to the ripple div with 2 eventsmousedown(start the ripple effect) andmouseup(trigger a timeout to remove thespan).
  • this.renderRippleSpan() - It is the function call to rendering spans(what we call Ripple :P)

NOTE: For every new Click on the element a new span child will get appended to therippleContainer so it is necessary to clean up the span to reduce DOM's size.

  • On Assigning the css styles to therippleContainer it overlaps its parent element completely.So any click will first reach handlers of div withrippleContainer class followed by its parent.

On clicking the Button…

.ripple .rippleContainer span {
  transform: scale(0);
  border-radius: 100%;
  position: absolute;
  opacity: 0.75;
  background-color: #ffffff;
  animation: ripple 850ms;
}

@keyframes ripple {
  to {
    opacity: 0;
    transform: scale(2);
  }
}
showRipple = (e) => {
    const rippleContainer = e.currentTarget;
    const size = rippleContainer.offsetWidth;
    const pos = rippleContainer.getBoundingClientRect();
    const x = e.pageX - pos.x - (size / 2);
    const y = e.pageY - pos.y - (size / 2);
    const spanStyles = { top: y + 'px', left: x + 'px', height: size + 'px', width: size + 'px' };
    const count = this.state.count + 1;
    this.setState({
      spanStyles: {...this.state.spanStyles, [count] : spanStyles},
      count: count
    });
  }

 renderRippleSpan = () => {
    const {showRipple = false, spanStyles = {}} = this.state;
    const spanArray = Object.keys(spanStyles);
    if (spanArray && spanArray.length > 0) {
      return (
        spanArray.map((key, index) => {
          return <span key={'spanCount_' + index} className="" style={{ ...spanStyles[key]}}></span>
        })
      )
    } else {
      return null;
    }
  }

showRipple Explained…

  • We CallshowRipple(e) function when the button is clicked.
  • currentTarget gives the element on which the handler is placed in this case the div withrippleContainer class.
  • Then, we find the width of therippleContainer.
  • getBoundingClientRect() is a built in function which gives the element’s position in the page (right, top, bottom, left, width, height, x and y)
  • x and y is a const we used a math to calculate the point of intersection of mouse inside the Button.e.pageX ande.pageY gives the X and Y coordinates of point of contact with respect to whole page.
  • Then we add the above obtained info to thespanStyles object.And we will use that style as inline style to the span element.
  • As state update is asynchronous in React we increment the counter and make use of it to store each click’s unique styles intospanStyle property. And Finally we are setting thestate.
  • renderRippleSpan() - A fairly straight-forward function where we will render span for every styles, it is necessary because the user can click on the button simultaneously.

css Explained…

  • We target the inner span,transform: scale(0) zooms in the element to 0% and we add custom animation effect and give it a nameripple

Definingripple animation in keyframe. The animation lasts for 850ms and during the course of 850ms the span’s opacity should reduce to 0 from 0.75 (helps in hiding ripple at the end of ripple effect) and zoom should increase to 200% from 0% (to make the background color of the span visible).

Tadaaaaaa…..!

Now we have got the ripple effect working we have to remove thespan that has been created.Removing the span involves including debounce functionality to the code, because if the user makes continuous clicks even before the transition for the previous click is completed we should not delete all the spans because it will affect the span of the last clicks that are in progress.

So we wait for a specified period (say 2000ms) and if we don’t get any clicks we will go ahead with deleting the span from the DOM. Wait time should be greater than animation transition time.

cleanUp = () => {
  const initialState = this.initializeState();
  this.setState({ ...initialState });
}

callCleanUp = (cleanup, delay) => {
  return function() {
    clearTimeout(this.bounce);
    this.bounce = setTimeout(() => {
      cleanup();
    }, delay);
  }
}
  • We initiatecallCleanup onmouseup.
  • Note: callCleanup() returns a function.Returned function gets called at the time ofmouseup
  • A timeout function which helps in removing the span after specified delay.
  • Debounce.Whenever the user keeps on clicking the button we clear out the previous timeout that is set, so that it wont affect the transition in progress and once we go through ‘delay’ milliseconds without click we start removing span.
  • cleanup() resets the state back to its original state(i.e) without spans.

Follow me for Interesting Contents :P

My Twitter

Find a working CodePen Example here

That’s all Folks!

Discussion (0)