<div class="container mt-5"> <h2>CSS-Only Ripple Effects with Progressive JS Enhancement</h2> <h3>Works with Any Color</h3> <div class="row"> <div class="col"> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-secondary btn-lg btn-block"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-success btn-lg btn-block"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-info btn-lg btn-block"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-warning btn-lg btn-block"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-danger btn-lg btn-block"> Click Me </button> </div> </div> </div> <h3>Works with Inputs <small>(Doesn't require a pseudo-element)</small></h3> <div class="row"> <div class="col"> <div class="form-group"> <input type="submit" class="btn btn-primary btn-lg btn-block mb-2" value="Submit Me"/> </div> </div> <div class="col"> <div class="form-group"> <input type="submit" class="btn btn-primary btn-lg btn-block mb-2 disabled" value="Can't Submit Me"/> </div> </div> </div> <h3>Works without JS</h3> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block no-click-fx"> Click Me </button> </div> <h3>Easy to Customize</h3> <div class="row"> <div class="col"> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block dark-ripple"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block light-ripple"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block red-ripple"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block fuzzy-ripple"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block fast-ripple"> Click Me </button> </div> </div> <div class="col"> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block slow-ripple"> Click Me </button> </div> </div> </div> <h3>Easy to Extend to Other Effects</h3> <div class="row"> <div class="col-sm-4"> <div class="form-group"> <input class="form-control"/> </div> </div> <div class="col-sm-4"> <div class="form-group"> <input class="form-control"/> </div> </div> <div class="col-sm-4"> <div class="form-group"> <input class="form-control"/> </div> </div> </div> <h3>Why This Works</h3> <p>I created this ripple effect using <b>only</b> <code>background-image</code>, <code>background-size</code>, and <code>background-position</code>, which are all animateable properties.</p> <p>This allows the effect to work directly on elements without any wrappers or pseudo-elements, and without js needing to kick off the animation.</p> <h4>Break the Effect Down Into Stages</h4> <p>We want to create a ripple outward, and then fade back to the buttons normal color, so lets handle these two things separately.</p> <p class="alert alert-warning"><small ><b>Note:</b> Animations below are mocked up using a pseudo element so overflow is visible. The real ripple effect only uses the background css properties.</small></p> <p class="alert alert-danger"><small ><b>Note:</b> For some reason touching any of the animations in mobile safari scrolls you to the top. I'm sorry about that, I am not sure why that's happening.</small></p> <h5>1. The Ripple</h5> <p>The ripple outward can be accomplished with a <code>radial-gradient</code> that we start at size 0% and expand to cover the whole button. Our radial gradient should be drawn as a circle that touches the nearest side and stays in the center: <code>radial-gradient(circle closest-side at center, ...</code>. Now we just need to make a box that is large enough for the circle to encompass the button. Most buttons are wider than tall, and never wider than <code>100vw</code>, so if our ripple background grows to be <code>sqrt(2) * 100% = 141%</code> wide and <code>100vw</code> tall, the circle should cover the entire button.</p> <p><b>Ripple Gradient Visual</b></p> <div class="figure figure-ripple figure-hover-animated"> <button class="btn btn-primary css-only">Button<span class="mockup"></span></button> </div> <h5>2. The Fade</h5> <p>The fade afterwards is tricky because we can't change background images opacity. However we can simulate that if we take a <code>linear-gradient</code> that starts with the ripple color and fades to transparent, and then move that over the button from the opaque end to the transparent end. If our gradient is long enough, it won't be obvious the background isn't fading uniformly. How long of a gradient do we need to make to convince you its uniform? Lets try a few different sizes.</p> <div class="row figure-set"> <div class="col col-xs-12"> <p><b>3 x height</b></p> <div class="figure figure-linear-fade figure-hover-animated" style="--bg-height-multiplier: 3;"> <button class="btn btn-primary css-only">Button<span class="mockup"></span></button> </div> </div> <div class="col col-xs-12"> <p><b>5 x height</b></p> <div class="figure figure-linear-fade figure-hover-animated" style="--bg-height-multiplier: 5;"> <button class="btn btn-primary css-only">Button<span class="mockup"></span></button> </div> </div> <div class="col col-xs-12"> <p><b>10 x height</b></p> <div class="figure figure-linear-fade figure-hover-animated" style="--bg-height-multiplier: 10;"> <button class="btn btn-primary css-only">Button<span class="mockup"></span></button> </div> </div> </div> <p>10 x height looks the most uniform, but 5x is good enough when the animation is fast, and is probably easier on memory.</p> <h4>Putting It Together</h4> <p>So we have a trick to create a ripple and a trick to emulate a fade, and we want to combine them into one animation. Thankfully we can add multiple background-images and set their properties individually. So we add both images and hide one of them by making its background size zero. Then in the middle we can "switch" the images seamlessly by changing their background sizes over a tiny time period (like in a keyframe 0.001% after the previous one).</p> <div class="row figure-set"> <div class="col col-xs-12"> <p><b>With Overflow Visible</b></p> <div class="figure figure-hover-animated figure-combined"> <button class="btn btn-primary css-only">Button<span class="mockup"></span></button> </div> </div> <div class="col col-xs-12"> <p><b>With Overflow Hidden</b></p> <div class="figure figure-hover-animated figure-combined figure-overflow-hidden"> <button class="btn btn-primary css-only">Button<span class="mockup"></span></button> </div> </div> </div> <h4>Improve with JS and CSS Variables</h4> <p>If we capture the click event on the button, we can calculate the offset of the click and inject them using css custom properties. Then we can access those in css using <code>var(name-of-var, default-value)</code>. If js isn't enabled, the default-value will be used and our animation will ripple outward from the center like normal. For this demonstration I calculate 5 things:</p> <ul> <li><b>--click-offset-x</b>: the distance of the click in px from the left edge of the button.</li> <li><b>--click-offset-y</b>: the distance of the click in px from the top edge of the button.</li> <li><b>--click-max-r</b>: the maximum distance from the click to any corner of the button, important to ensure our ripple becomes large enough to cover the button.</li> <li><b>--click-el-w</b>: the width of the element (this is necessary to position the center of the background-image relative to the edges of the button -- starting from 0% and adding the offset would align the left edge of the background and not the center).</li> <li><b>--click-el-h</b>: the height of the element (same reason as above).</li> </ul> <div class="row"> <div class="col col-xs-12"> <p><b>With Overflow Visible</b></p> <div class="figure figure-combined figure-click-animated"> <button class="btn btn-primary linked-ripple">Button<span class="mockup"></span></button> </div> </div> <div class="col col-xs-12"> <p><b>With Overflow Hidden</b></p> <div class="figure figure-combined figure-click-animated figure-overflow-hidden"> <button class="btn btn-primary linked-ripple">Button<span class="mockup"></span></button> </div> </div> </div> <p>My script adds 3 helper classes during interaction:</p> <ul> <li><b>pre-click-fx</b>: Gets applied 1 frame before <code>click-fx</code>, allows you to reset the animation so that e.g. a focused button can be clicked again.</li> <li><b>click-fx</b>: Gets applied while the animation should happen and <b>after</b> the css variables have taken effect. Some browsers like Mobile Safari have latency between when a css variable is updated and when the stylesheet sees the new value.</li> <li><b>post-click-fx</b>: Gets applied after the animation finishes and does not get removed until the element loses focus. This allows you to prevent your text inputs from rippling again while a user is clicking around it to e.g. edit text.</li> </ul> </div>

!