CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-material-design-lite

Material Design Components in CSS, JS and HTML providing a comprehensive implementation of Google's Material Design specification for web applications.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

visual-effects.mddocs/

Visual Effects

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.

Capabilities

Material Ripple

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);

Ripple Effect Customization

// 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>

Performance Optimization

// 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);

Ripple Constants

/**
 * 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: '';
}

Animation Utilities

// 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);
}

Accessibility Considerations

// 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);

Ripple Effect Themes

// 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');

docs

component-management.md

data-display-components.md

feedback-components.md

form-components.md

index.md

layout-components.md

navigation-components.md

visual-effects.md

tile.json