Material Design Components in CSS, JS and HTML providing a comprehensive implementation of Google's Material Design specification for web applications.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Visual enhancement components including ripple effects and animations that provide tactile feedback and smooth transitions. These effects enhance the user experience by providing immediate visual feedback for interactions.
Touch ripple effect component that creates expanding circular animations on user interactions.
/**
* Material Design ripple effect component
* CSS Class: mdl-js-ripple-effect
* Widget: false
*/
interface MaterialRipple {
/**
* Get current animation frame count
* @returns Current frame count number
*/
getFrameCount(): number;
/**
* Set animation frame count
* @param frameCount - New frame count value
*/
setFrameCount(frameCount: number): void;
/**
* Get the DOM element used for ripple effect
* @returns HTMLElement representing the ripple
*/
getRippleElement(): HTMLElement;
/**
* Set ripple animation coordinates
* @param x - X coordinate for ripple center
* @param y - Y coordinate for ripple center
*/
setRippleXY(x: number, y: number): void;
/**
* Set ripple styling for animation phase
* @param start - Whether this is the start or end of animation
*/
setRippleStyles(start: boolean): void;
/** Handle animation frame updates */
animFrameHandler(): void;
}HTML Structure:
<!-- Button with ripple effect -->
<button class="mdl-button mdl-js-button mdl-js-ripple-effect">
Ripple Button
</button>
<!-- Checkbox with ripple effect -->
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="checkbox-ripple">
<input type="checkbox" id="checkbox-ripple" class="mdl-checkbox__input">
<span class="mdl-checkbox__label">Check with ripple</span>
</label>
<!-- Menu with ripple effect -->
<ul class="mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect"
for="demo-menu-button">
<li class="mdl-menu__item">Menu Item 1</li>
<li class="mdl-menu__item">Menu Item 2</li>
</ul>
<!-- Custom element with ripple -->
<div class="custom-element mdl-js-ripple-effect" tabindex="0">
Click me for ripple effect
</div>Usage Examples:
Since ripple effects are largely automatic, direct API usage is rare, but here are some advanced use cases:
// Access ripple instance (rarely needed)
const rippleElement = document.querySelector('.mdl-js-ripple-effect');
// Note: MaterialRipple instances are typically managed internally
// Programmatically trigger ripple effect
function triggerRipple(element, x, y) {
// Create a synthetic mouse event at specific coordinates
const event = new MouseEvent('mousedown', {
clientX: x,
clientY: y,
bubbles: true
});
element.dispatchEvent(event);
// Clean up with mouseup
setTimeout(() => {
const upEvent = new MouseEvent('mouseup', {
bubbles: true
});
element.dispatchEvent(upEvent);
}, 100);
}
// Add ripple effect to custom elements
function addRippleToElement(element) {
if (!element.classList.contains('mdl-js-ripple-effect')) {
element.classList.add('mdl-js-ripple-effect');
componentHandler.upgradeElement(element);
}
}
// Remove ripple effect
function removeRippleFromElement(element) {
element.classList.remove('mdl-js-ripple-effect');
// Remove ripple container if it exists
const rippleContainer = element.querySelector('.mdl-ripple-container');
if (rippleContainer) {
rippleContainer.remove();
}
}
// Custom ripple colors
function setRippleColor(element, color) {
const style = document.createElement('style');
const className = 'ripple-' + Math.random().toString(36).substr(2, 9);
element.classList.add(className);
style.textContent = `
.${className} .mdl-ripple {
background: ${color};
}
`;
document.head.appendChild(style);
}
// Usage examples
const customButton = document.querySelector('#custom-button');
addRippleToElement(customButton);
setRippleColor(customButton, '#ff4081');
// Trigger ripple on center of element
const rect = customButton.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
triggerRipple(customButton, centerX, centerY);// Custom ripple implementation for non-standard elements
class CustomRippleManager {
constructor() {
this.ripples = new Map();
this.setupGlobalListeners();
}
setupGlobalListeners() {
document.addEventListener('mousedown', (event) => {
if (event.target.matches('[data-custom-ripple]')) {
this.createRipple(event.target, event);
}
});
document.addEventListener('mouseup', () => {
this.fadeAllRipples();
});
document.addEventListener('mouseleave', () => {
this.fadeAllRipples();
});
}
createRipple(element, event) {
const rect = element.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
const ripple = document.createElement('div');
ripple.className = 'custom-ripple';
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: scale(0);
pointer-events: none;
transition: transform 0.6s, opacity 0.6s;
`;
// Ensure element has relative positioning
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
// Ensure element has overflow hidden
element.style.overflow = 'hidden';
element.appendChild(ripple);
// Store ripple reference
this.ripples.set(ripple, { element, startTime: Date.now() });
// Trigger animation
requestAnimationFrame(() => {
ripple.style.transform = 'scale(2)';
});
}
fadeAllRipples() {
this.ripples.forEach((info, ripple) => {
const elapsed = Date.now() - info.startTime;
// Only fade if ripple has been visible for minimum time
if (elapsed > 100) {
ripple.style.opacity = '0';
setTimeout(() => {
if (ripple.parentNode) {
ripple.parentNode.removeChild(ripple);
}
this.ripples.delete(ripple);
}, 600);
}
});
}
}
// Initialize custom ripple manager
const customRippleManager = new CustomRippleManager();
// Usage: Add data-custom-ripple attribute to elements
// <div data-custom-ripple class="my-button">Custom Ripple</div>// Optimized ripple effect with requestAnimationFrame
class OptimizedRipple {
constructor(element) {
this.element = element;
this.isAnimating = false;
this.setupListeners();
}
setupListeners() {
this.element.addEventListener('mousedown', (event) => {
if (!this.isAnimating) {
this.startRipple(event);
}
});
this.element.addEventListener('mouseup', () => {
this.endRipple();
});
this.element.addEventListener('mouseleave', () => {
this.endRipple();
});
}
startRipple(event) {
this.isAnimating = true;
const rect = this.element.getBoundingClientRect();
const rippleContainer = this.getRippleContainer();
const ripple = document.createElement('div');
ripple.className = 'optimized-ripple';
const size = Math.max(rect.width, rect.height) * 2;
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: scale(0);
opacity: 1;
pointer-events: none;
`;
rippleContainer.appendChild(ripple);
this.currentRipple = ripple;
// Use requestAnimationFrame for smooth animation
this.animateRipple(ripple, 0);
}
animateRipple(ripple, startTime) {
if (!startTime) startTime = performance.now();
const elapsed = performance.now() - startTime;
const duration = 600;
const progress = Math.min(elapsed / duration, 1);
// Easing function
const easeOut = 1 - Math.pow(1 - progress, 3);
ripple.style.transform = `scale(${easeOut})`;
if (progress < 1 && this.isAnimating) {
requestAnimationFrame(() => this.animateRipple(ripple, startTime));
}
}
endRipple() {
if (this.currentRipple && this.isAnimating) {
this.isAnimating = false;
// Fade out
this.currentRipple.style.transition = 'opacity 0.3s';
this.currentRipple.style.opacity = '0';
setTimeout(() => {
if (this.currentRipple && this.currentRipple.parentNode) {
this.currentRipple.parentNode.removeChild(this.currentRipple);
}
this.currentRipple = null;
}, 300);
}
}
getRippleContainer() {
let container = this.element.querySelector('.ripple-container');
if (!container) {
container = document.createElement('div');
container.className = 'ripple-container';
container.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
pointer-events: none;
`;
this.element.appendChild(container);
// Ensure parent has relative positioning
if (getComputedStyle(this.element).position === 'static') {
this.element.style.position = 'relative';
}
}
return container;
}
}
// Apply optimized ripple to elements
function addOptimizedRipple(element) {
if (!element.optimizedRipple) {
element.optimizedRipple = new OptimizedRipple(element);
}
}
// Usage
document.querySelectorAll('[data-optimized-ripple]').forEach(addOptimizedRipple);/**
* Material Ripple constants and configuration
*/
interface RippleConstants {
/** Initial scale transform for ripple start */
INITIAL_SCALE: 'scale(0.0001, 0.0001)';
/** Initial size for ripple element */
INITIAL_SIZE: '1px';
/** Initial opacity for ripple start */
INITIAL_OPACITY: '0.4';
/** Final opacity for ripple end */
FINAL_OPACITY: '0';
/** Final scale transform for ripple end */
FINAL_SCALE: '';
}// Utility functions for working with animations
class AnimationUtils {
static easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
static easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
static animate(element, properties, duration, easing = 'easeOut') {
const startTime = performance.now();
const startValues = {};
// Get initial values
Object.keys(properties).forEach(prop => {
const currentValue = this.getNumericValue(element, prop);
startValues[prop] = currentValue;
});
const easingFunction = typeof easing === 'string' ?
this[easing] || this.easeOutCubic : easing;
const step = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easingFunction(progress);
Object.keys(properties).forEach(prop => {
const startValue = startValues[prop];
const endValue = properties[prop];
const currentValue = startValue + (endValue - startValue) * easedProgress;
this.setProperty(element, prop, currentValue);
});
if (progress < 1) {
requestAnimationFrame(step);
}
};
requestAnimationFrame(step);
}
static getNumericValue(element, property) {
const style = getComputedStyle(element);
const value = style[property];
return parseFloat(value) || 0;
}
static setProperty(element, property, value) {
switch (property) {
case 'scale':
element.style.transform = `scale(${value})`;
break;
case 'opacity':
element.style.opacity = value;
break;
default:
element.style[property] = value + 'px';
}
}
}
// Usage with ripple effects
function createAnimatedRipple(element, event) {
const rect = element.getBoundingClientRect();
const ripple = document.createElement('div');
// Setup ripple
const size = Math.max(rect.width, rect.height) * 2;
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: scale(0);
opacity: 0.4;
pointer-events: none;
`;
element.appendChild(ripple);
// Animate with custom easing
AnimationUtils.animate(ripple, { scale: 1 }, 600, AnimationUtils.easeOutCubic);
// Fade out after delay
setTimeout(() => {
AnimationUtils.animate(ripple, { opacity: 0 }, 300, (t) => t);
setTimeout(() => {
if (ripple.parentNode) {
ripple.parentNode.removeChild(ripple);
}
}, 300);
}, 400);
}// Respect user preferences for reduced motion
function respectMotionPreferences() {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
// Disable ripple effects
document.querySelectorAll('.mdl-js-ripple-effect').forEach(element => {
element.classList.remove('mdl-js-ripple-effect');
element.classList.add('mdl-js-ripple-effect--disabled');
});
// Add CSS to disable animations
const style = document.createElement('style');
style.textContent = `
.mdl-js-ripple-effect--disabled .mdl-ripple,
.custom-ripple,
.optimized-ripple {
animation: none !important;
transition: none !important;
}
`;
document.head.appendChild(style);
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', respectMotionPreferences);
// Handle dynamic preference changes
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', respectMotionPreferences);// Different ripple themes for various contexts
const RippleThemes = {
light: {
background: 'rgba(0, 0, 0, 0.1)',
duration: 600
},
dark: {
background: 'rgba(255, 255, 255, 0.3)',
duration: 600
},
accent: {
background: 'rgba(255, 64, 129, 0.3)',
duration: 800
},
success: {
background: 'rgba(76, 175, 80, 0.3)',
duration: 600
},
warning: {
background: 'rgba(255, 152, 0, 0.3)',
duration: 600
},
error: {
background: 'rgba(244, 67, 54, 0.3)',
duration: 600
}
};
function applyRippleTheme(element, theme) {
const themeConfig = RippleThemes[theme];
if (!themeConfig) return;
element.addEventListener('mousedown', (event) => {
createThemedRipple(element, event, themeConfig);
});
}
function createThemedRipple(element, event, theme) {
const rect = element.getBoundingClientRect();
const ripple = document.createElement('div');
const size = Math.max(rect.width, rect.height) * 2;
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: ${theme.background};
border-radius: 50%;
transform: scale(0);
opacity: 1;
pointer-events: none;
transition: transform ${theme.duration}ms cubic-bezier(0.4, 0, 0.2, 1),
opacity ${theme.duration * 0.5}ms ease-out;
`;
element.appendChild(ripple);
requestAnimationFrame(() => {
ripple.style.transform = 'scale(1)';
});
setTimeout(() => {
ripple.style.opacity = '0';
setTimeout(() => {
if (ripple.parentNode) {
ripple.parentNode.removeChild(ripple);
}
}, theme.duration * 0.5);
}, theme.duration * 0.7);
}
// Usage
applyRippleTheme(document.querySelector('#success-button'), 'success');
applyRippleTheme(document.querySelector('#error-button'), 'error');