This code demonstrates how to create a material design-like button ripple effect using CSS. This effect is achieved by utilizing the background-image
, background-size
, and background-position
properties, which are all animateable properties.
The code allows the ripple effect to be applied to buttons of different colors and also supports inputs without the need for pseudo-elements. One of the key advantages of this code is that it works without requiring JavaScript, making it lightweight and efficient.
Additionally, the code is easy to customize, enabling you to apply different ripple effects such as dark, light, red, fuzzy, fast, and slow ripples. Furthermore, the code provides a foundation for extending the implementation to include other effects.
By combining radial and linear gradients, the code creates a ripple effect that expands outward and fades back to the button’s normal color. The animation is achieved by dynamically adjusting the background sizes over a small time period.
How to Create Material Design Like Button Ripple Effect
1. First of all, load the Bootstrap CSS by adding the following CDN link into the head tag of your HTML document.
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.css'>
2. After that, create the HTML button element as follows:
<button class="btn btn-primary btn-lg btn-block"> Your Awesome Button </button> <!-- Ripple Effect without JS --> <button class="btn btn-primary btn-lg btn-block no-click-fx"> Ripple Button </button>
3. Use the following CSS code to apply ripple effect on buttons.
.btn { --ripple-color: rgba(0, 0, 0, 0.3); --ripple-duration: 1.5s; --ripple-fuzz: 1px; border-color: transparent !important; /* Just the ripple goes to the edges */ } .btn:focus:not(:disabled):not(.disabled), .btn:active:not(:disabled):not(.disabled), .btn.click-fx:not(:disabled):not(.disabled) { background-image: radial-gradient(circle closest-side at center, var(--ripple-color) 0%, var(--ripple-color) calc(100% - var(--ripple-fuzz, 0px)), transparent 100%), linear-gradient(180deg, var(--ripple-color) 10%, transparent 90%); background-size: 0% 0%, 0% 0%; background-repeat: no-repeat; background-origin: border-box; -webkit-animation: button-ripple var(--ripple-duration) ease-in; animation: button-ripple var(--ripple-duration) ease-in; } .btn.pre-click-fx { -webkit-animation: none !important; animation: none !important; } @-webkit-keyframes button-ripple { 0% { background-size: 0% 0%, 0% 0%; background-position: calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)) calc(50% - var(--click-el-h, 0px)/2 + var(--click-offset-y, 0px)), 0% 0%; } 33% { background-size: calc(2 * var(--click-max-r, 71%)) calc(2 * var(--click-max-r, 200vw)), 0% 0%; background-size: calc(2 * var(--click-max-r, 71%)) 100vmax, 0% 0%; background-position: calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)) calc(50% - var(--click-el-h, 0px)/2 + var(--click-offset-y, 0px)), 0% 0%; } 33.1% { background-size: 0% 0%, 100% 1000%; } 100% { background-size: 0% 0%, 100% 1000%; background-position: 0% 0%, 0% 100%; } } @keyframes button-ripple { 0% { background-size: 0% 0%, 0% 0%; background-position: calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)) calc(50% - var(--click-el-h, 0px)/2 + var(--click-offset-y, 0px)), 0% 0%; } 33% { background-size: calc(2 * var(--click-max-r, 71%)) calc(2 * var(--click-max-r, 200vw)), 0% 0%; background-size: calc(2 * var(--click-max-r, 71%)) 100vmax, 0% 0%; background-position: calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)) calc(50% - var(--click-el-h, 0px)/2 + var(--click-offset-y, 0px)), 0% 0%; } 33.1% { background-size: 0% 0%, 100% 1000%; } 100% { background-size: 0% 0%, 100% 1000%; background-position: 0% 0%, 0% 100%; } } .dark-ripple { --ripple-color: rgba(0, 0, 0, 0.6); } .light-ripple { --ripple-color: rgba(255, 255, 255, 0.3); } .red-ripple { --ripple-color: rgba(255, 0, 0, 0.3); } .fuzzy-ripple { --ripple-fuzz: 30px; } .fast-ripple { --ripple-duration: 1s; } .slow-ripple { --ripple-duration: 3s; } .form-control { border-radius: 0; border: none; background-color: rgba(0, 0, 0, 0.1); transition: background-size 0.5s, background-color 0.5s, border-color 0.25s, color 0.2s, box-shadow 0.25s; } .form-control, .form-control:hover, .form-control:focus, .form-control:active { background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0.35) 100%), linear-gradient(0deg, #007bff 0%, #007bff 100%); background-repeat: no-repeat; background-position: 50% 100%, 50% 100%; background-size: 100% 2px, 0% 2px; } .form-control:focus, .form-control:active, .form-control.click-fx { background-size: 100% 2px, 100% 2px; -webkit-animation: input-ripple 0.5s ease-in; animation: input-ripple 0.5s ease-in; } .form-control.pre-click-fx:not(.post-click-fx) { -webkit-animation: none !important; animation: none !important; } @-webkit-keyframes input-ripple { 0% { background-position: 50% 100%, calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)) 100%; background-size: 100% 2px, 0% 2px, 0% 0%; } 100% { background-position: 50% 100%, calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)) 100%; background-size: 100% 2px, calc(2 * var(--click-max-r, 71%)) 2px; } } @keyframes input-ripple { 0% { background-position: 50% 100%, calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)) 100%; background-size: 100% 2px, 0% 2px, 0% 0%; } 100% { background-position: 50% 100%, calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)) 100%; background-size: 100% 2px, calc(2 * var(--click-max-r, 71%)) 2px; } } .figure { position: relative; text-align: center; width: 100%; padding: 75px 75px; border: 2px dashed #ccc; margin-bottom: 24px; overflow: hidden; } .figure caption { position: absolute; top: 12px; left: 12px; right: 12px; color: inherit; } .figure .btn { position: relative; z-index: 0; background-image: none !important; } .figure .btn .mockup { content: ''; display: block; position: absolute; top: 50%; left: 50%; border: 2px dashed #111; transform: translate(-50%, -50%); background-repeat: no-repeat; z-index: -1; } .figure.figure-hover-animated:after, .figure.figure-click-animated:after { position: absolute; bottom: 0px; left: 0px; width: 100%; background: rgba(255, 255, 255, 0.5); text-align: center; color: inherit; transition: opacity 0.25s; } .figure.figure-hover-animated:hover:after, .figure.figure-click-animated:hover:after, .figure-set:hover .figure.figure-hover-animated:after, .figure-set:hover .figure.figure-click-animated:after { opacity: 0; } .figure.figure-hover-animated:after { content: 'Hover for Animation'; } .figure.figure-click-animated:after { content: 'Click Button for Animation'; } .figure.figure-overflow-hidden .btn { overflow: hidden !important; } .figure.figure-overflow-hidden .btn .mockup { border-width: 0px !important; } .figure.figure-ripple .btn .mockup { width: 0px; height: 0px; border-color: transparent; background-image: radial-gradient(circle closest-side at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.3) 99%, transparent 100%); transition: width 1s ease-in, height 1s ease-in, border-color 0.25s; } .figure.figure-ripple.figure-hover-animated:hover .btn .mockup, .figure.figure-ripple.figure-click-animated .btn:focus, .figure.figure-ripple .mockup, .figure.figure-ripple.figure-click-animated .btn:active .mockup { width: 141%; height: 240px; border-color: #111; } .figure.figure-linear-fade .btn .mockup { width: calc(100% + 4px); height: calc(100% * var(--bg-height-multiplier)); top: 0; transform: translate(-50%, 0%) translateY(-2px); background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) calc(100% / var(--bg-height-multiplier)), transparent calc(100% - 100% / var(--bg-height-multiplier))); transition: transform 1s ease-in; } .figure-set:hover .figure.figure-linear-fade.figure-hover-animated .btn .mockup, .figure.figure-linear-fade.figure-hover-animated:hover .btn .mockup, .figure.figure-linear-fade.figure-click-animated .btn:focus .mockup, .figure.figure-linear-fade.figure-click-animated .btn:active .mockup { transform: translate(-50%, calc(-1 * (100% - 100% / var(--bg-height-multiplier)))) translateY(2px); } .figure.figure-combined .btn .mockup { width: 0%; height: 0%; background-image: radial-gradient(circle closest-side at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.3) 99%, transparent 100%), linear-gradient(180deg, rgba(0, 0, 0, 0.3) 20%, transparent 80%); opacity: 0; } .figure-set:hover .figure.figure-combined.figure-hover-animated .btn, .figure.figure-combined.figure-hover-animated:hover .btn, .figure.figure-combined.figure-click-animated .btn:focus, .figure.figure-combined.figure-click-animated .btn:active, .figure.figure-combined .btn.click-fx { -webkit-animation: empty-animation 5s; animation: empty-animation 5s; } .figure-set:hover .figure.figure-combined.figure-hover-animated .btn.pre-click-fx, .figure.figure-combined.figure-hover-animated:hover .btn.pre-click-fx, .figure.figure-combined.figure-click-animated .btn:focus.pre-click-fx, .figure.figure-combined.figure-click-animated .btn:active.pre-click-fx, .figure.figure-combined .btn.click-fx.pre-click-fx { -webkit-animation: none !important; animation: none !important; } .figure-set:hover .figure.figure-combined.figure-hover-animated .btn.pre-click-fx .mockup, .figure.figure-combined.figure-hover-animated:hover .btn.pre-click-fx .mockup, .figure.figure-combined.figure-click-animated .btn:focus.pre-click-fx .mockup, .figure.figure-combined.figure-click-animated .btn:active.pre-click-fx .mockup, .figure.figure-combined .btn.click-fx.pre-click-fx .mockup { -webkit-animation: none !important; animation: none !important; } .figure-set:hover .figure.figure-combined.figure-hover-animated .btn .mockup, .figure.figure-combined.figure-hover-animated:hover .btn .mockup, .figure.figure-combined.figure-click-animated .btn:focus .mockup, .figure.figure-combined.figure-click-animated .btn:active .mockup, .figure.figure-combined .btn.click-fx .mockup { -webkit-animation: figure-combined 5s ease-in forwards; animation: figure-combined 5s ease-in forwards; background-size: 0px 0px; } @-webkit-keyframes figure-combined { 0% { top: calc(50% - var(--click-el-h, 0px)/2 + var(--click-offset-y, 0px)); left: calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)); width: 0%; height: 0%; background-size: 100% 100%, 0% 0%; opacity: 1; transform: translate(0px, 0px); } 20% { width: calc(2 * var(--click-max-r, 71%)); height: calc(2 * var(--click-max-r, 95px)); border-color: #111; transform: translate(calc(-1 * var(--click-max-r, 51px)), calc(-1 * var(--click-max-r, 95px))); } 30% { border-color: transparent; } 40% { top: calc(50% - var(--click-el-h, 0px)/2 + var(--click-offset-y, 0px)); left: calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)); width: calc(2 * var(--click-max-r, 71%)); height: calc(2 * var(--click-max-r, 95px)); background-size: 100% 100%, 0% 0%; border-color: transparent; transform: translate(calc(-1 * var(--click-max-r, 51px)), calc(-1 * var(--click-max-r, 95px))); } 40.001% { top: 0%; left: 50%; width: calc(100% + 4px); height: 500%; background-size: 0% 0%, 100% 100%; transform: translate(-38px, 0px) translateY(-2px); } 45% { border-color: transparent; } 55% { border-color: #111; } 60% { transform: translate(-38px, 0px) translateY(-2px); } 80% { transform: translate(-38px, -144px) translateY(2px); opacity: 1; } 100% { top: 0%; width: calc(100% + 4px); height: 500%; background-size: 0% 0%, 100% 100%; transform: translate(-38px, -144px) translateY(2px); opacity: 0; } } @keyframes figure-combined { 0% { top: calc(50% - var(--click-el-h, 0px)/2 + var(--click-offset-y, 0px)); left: calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)); width: 0%; height: 0%; background-size: 100% 100%, 0% 0%; opacity: 1; transform: translate(0px, 0px); } 20% { width: calc(2 * var(--click-max-r, 71%)); height: calc(2 * var(--click-max-r, 95px)); border-color: #111; transform: translate(calc(-1 * var(--click-max-r, 51px)), calc(-1 * var(--click-max-r, 95px))); } 30% { border-color: transparent; } 40% { top: calc(50% - var(--click-el-h, 0px)/2 + var(--click-offset-y, 0px)); left: calc(50% - var(--click-el-w, 0px)/2 + var(--click-offset-x, 0px)); width: calc(2 * var(--click-max-r, 71%)); height: calc(2 * var(--click-max-r, 95px)); background-size: 100% 100%, 0% 0%; border-color: transparent; transform: translate(calc(-1 * var(--click-max-r, 51px)), calc(-1 * var(--click-max-r, 95px))); } 40.001% { top: 0%; left: 50%; width: calc(100% + 4px); height: 500%; background-size: 0% 0%, 100% 100%; transform: translate(-38px, 0px) translateY(-2px); } 45% { border-color: transparent; } 55% { border-color: #111; } 60% { transform: translate(-38px, 0px) translateY(-2px); } 80% { transform: translate(-38px, -144px) translateY(2px); opacity: 1; } 100% { top: 0%; width: calc(100% + 4px); height: 500%; background-size: 0% 0%, 100% 100%; transform: translate(-38px, -144px) translateY(2px); opacity: 0; } }
4. You can also use the following JavaScript code to create ripple effects dynamically.
// If you want to use this in your projects, feel free // to copy and paste it. It is dependency free and // should be cross-browser compatible. ^^ ;(function() { // BEGIN POLYFILLS // .matches() Polyfill if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; } // .closest Polyfill if (!Element.prototype.closest) { Element.prototype.closest = function(s) { var el = this; if (!document.documentElement.contains(el)) return null; do { if (el.matches(s)) return el; el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); return null; }; } // END POLYFILLS // BEGIN MICRO EVENT LIBRARY var eventHandlers = {}; window.clickFxHandlers = eventHandlers; function decorateHandler(fn, selector) { return function(e) { e = e || window.event; if (selector && !e.target.matches(selector)) { return; } fn(e); }; } function bind(el, eventsOrSelector, eventsOrFn, fnOrUndefined) { var selector = false; var events = eventsOrSelector; var fn = eventsOrFn; if (arguments.length === 4) { selector = eventsOrSelector; events = eventsOrFn; fn = fnOrUndefined; } var eventList = events.split(' '); var handler = decorateHandler(fn, selector); for (var i in eventList) { var event = eventList[i];; eventHandlers[event] = eventHandlers[event] || []; eventHandlers[event].push({ 'original': fn, 'decorated': handler, 'el': el, 'selector': selector }); el.addEventListener(event, handler); } } function unbind(el, eventsOrSelector, eventsOrFn, fnOrUndefined) { var selector = false; var events = eventsOrSelector; var fn = eventsOrFn; if (arguments.length === 4) { selector = eventsOrSelector; events = eventsOrFn; fn = fnOrUndefined; } var eventList = events.split(' '); for (var i in eventList) { var event = eventList[i]; if ('undefined' !== typeof eventHandlers[event]) { var handlers = eventHandlers[event]; var hIndex = handlers.findIndex(function(handler) { return handler.original === fn && handler.selector === selector && handler.el === el; }); if (-1 !== hIndex) { el.removeEventListener(event, handlers[hIndex].decorated); handlers.splice(hIndex, 1); } } } } function bindOnce(el, eventsOrSelector, eventsOrFn, fnOrUndefined) { var selector = false; var events = eventsOrSelector; var fn = eventsOrFn; if (arguments.length === 4) { selector = eventsOrSelector; events = eventsOrFn; fn = fnOrUndefined; } bind(el, selector, events, fn); bind(el, selector, events, function oneFn() { unbind(el, selector, events, fn); unbind(el, selector, events, oneFn); }); } // END MICRO EVENT LIBRARY // BEGIN CLICK-FX // Detect css animations function hasCssAnimation(el) { // get a collection of all children including self var items = [el].concat(Array.prototype.slice.call(el.getElementsByTagName("*"))); // go through each item in reverse (faster) for (var i = items.length; i--;) { // get the applied styles var style = window.getComputedStyle(items[i], null); // read the animation duration - defaults to 0 var animDuration = parseFloat(style.getPropertyValue('animation-duration') || '0'); // if we have any duration greater than 0, an animation exists if (animDuration > 0) { return true; } } return false; } // Calculate and apply click-fx function applyClickFx(el, clickCoords) { if ('string' === typeof el) { document.querySelectorAll(el).forEach(function (oneEl) { applyClickFx(oneEl, clickCoords); }); return; } if (el.classList.contains('click-fx') || el.classList.contains('pre-click-fx') || el.classList.contains('no-click-fx')) { return; } var cssVars = {}; if (clickCoords) { let elOffset = el.getBoundingClientRect(); let clickOffset = { x: Math.round(clickCoords.x - elOffset.left), y: Math.round(clickCoords.y - elOffset.top) }; cssVars['--click-offset-x'] = clickOffset.x + 'px'; cssVars['--click-offset-y'] = clickOffset.y + 'px'; cssVars['--click-max-r'] = Math.round(Math.sqrt( Math.pow(Math.max(clickOffset.x, el.offsetWidth - clickOffset.x), 2) + Math.pow(Math.max(clickOffset.y, el.offsetHeight - clickOffset.y), 2) )) + 'px'; cssVars['--click-el-w'] = el.offsetWidth + 'px'; cssVars['--click-el-h'] = el.offsetHeight + 'px'; } for (var cssVar in cssVars) { el.style.setProperty(cssVar, cssVars[cssVar]); } el.classList.add('pre-click-fx'); requestAnimationFrame(function(){ // If its ignoring reset, exit early if (hasCssAnimation(el)) { el.classList.remove('pre-click-fx'); return; } el.classList.add('click-fx'); el.classList.remove('pre-click-fx'); bindOnce(el, 'animationend webkitAnimationEnd oAnimationEnd animationcancel webkitAnimationCancel oAnimationCancel', function(e) { el.classList.remove('click-fx'); for (var cssVar in cssVars) { el.style.removeProperty(cssVar); } if (el.matches('input:not([type=submit]):not([type=button]):not([type=reset]), textarea, select') && el === document.activeElement) { // This is a stateful element and so might have a // stateful effect el.classList.add('post-click-fx'); // Give other plugins a chance to do their magic to // the element (e.g. select2 hidden input) before checking // if it is still focused bind(document.querySelector('body'), 'mouseup touchend keyup', function longBlurHandler(e) { setTimeout(function() { if (el !== document.activeElement) { // No longer focused el.classList.remove('post-click-fx'); unbind(document.querySelector('body'), 'mouseup touchend keyup', longBlurHandler); } }, 0); }); } }); }); } // API window.clickFx = function (selector) { var lastEvent = null; bind(document.querySelector('body'), selector + ',' + selector.replace(/,/g, ' *,'), 'mousedown touchstart touchend focusin', function(e) { var ignoreEvent = false; if ( lastEvent && 'mousedown' === e.type && 'touchend' === lastEvent.type && e.target === lastEvent.target ) { // This is a mousedown fired automatically after a touchend on same target ignoreEvent = true; } else if ('touchend' === e.type) { ignoreEvent = true; } lastEvent = e; if (ignoreEvent) { return; } var el = e.target; if (!el.matches(selector)) { el = el.closest(selector); } var clickCoords = false; if ('focusin' !== e.type) { // This is a click event clickCoords = { x: e.clientX, y: e.clientY }; if (e.changedTouches && e.changedTouches[0]) { clickCoords.x = e.changedTouches[0].clientX; clickCoords.y = e.changedTouches[0].clientY; } } // Apply actual effect applyClickFx(el, clickCoords); // Check if this el should trigger effect on other els if ('undefined' !== typeof el.dataset.applyClickFx) { document.querySelectorAll(el.dataset.applyClickFx).forEach(function(otherEl) { applyClickFx(otherEl, clickCoords); }); let parent = el.parent; while (parent) { if ('undefined' !== parent.dataset.applyClickFx) { document.querySelectorAll(parent.dataset.applyClickFx).forEach(function(otherEl) { applyClickFx(otherEl, clickCoords); }); } parent = parent.parent; } } // Check if other els should be triggered by this el document.querySelectorAll('[data-subscribe-click-fx]').forEach(function(otherEl) { if (el.matches(otherEl.dataset.subscribeClickFx)) { applyClickFx(otherEl, clickCoords); } }); }); }; // END CLICK-FX // BEGIN DEMO (You can get rid of this) bind(document, 'DOMContentLoaded', function() { clickFx('a, input, textarea, button, .btn'); }); bind(document.querySelector('body'), '.linked-ripple, .linked-ripple *', 'mousedown touchstart touchend', function(e) { var targetEl = e.target; var clientX = e.clientX; var clientY = e.clientY; var targetElRect = targetEl.getBoundingClientRect(); var relativeOffset = { x: Math.round(clientX - targetElRect.left), y: Math.round(clientY - targetElRect.top) }; document.querySelectorAll('.linked-ripple').forEach(function(linkedEl) { if (linkedEl === targetEl) { return; } var elRect = linkedEl.getBoundingClientRect(); applyClickFx(linkedEl, { x: elRect.left + relativeOffset.x, y: elRect.top + relativeOffset.y }); }); }); if ('undefined' !== typeof window._l) { // Catch peoples eye when they see this in the preview Array.from(document.querySelectorAll('.btn:not(.css-only)')).slice(0, 14).forEach(function(el, i) { var elRect = el.getBoundingClientRect(); setTimeout(function() { applyClickFx(el, { x: elRect.left + 10, y: elRect.top + 10 }); }, i * 100); }); } // END DEMO })();
That’s all! hopefully, you have successfully created Material Design like button ripple effect in CSS. If you have any questions or suggestions, feel free to comment below.