Web APIs

JavaScript Vibration API: Complete Haptic Feedback Guide

Master the Vibration API in JavaScript for adding tactile feedback to web applications. Learn vibration patterns, user interaction, and mobile experiences.

By JavaScriptDoc Team
vibrationhapticmobilefeedbackjavascript

JavaScript Vibration API: Complete Haptic Feedback Guide

The Vibration API provides a simple way to control device vibration, enabling tactile feedback for enhanced user interactions, particularly on mobile devices.

Understanding the Vibration API

The Vibration API allows web applications to access the vibration hardware of devices, creating haptic feedback patterns for various user interactions.

// Check Vibration API support
if ('vibrate' in navigator) {
  console.log('Vibration API is supported');

  // Simple vibration for 200ms
  navigator.vibrate(200);
} else {
  console.log('Vibration API is not supported');
}

// Vibration patterns
// [vibrate, pause, vibrate, pause, ...]
navigator.vibrate([100, 50, 100, 50, 300]);

// Stop vibration
navigator.vibrate(0);
// or
navigator.vibrate([]);

// Check if vibration was successful
const vibrated = navigator.vibrate(100);
console.log('Vibration initiated:', vibrated);

Basic Vibration Patterns

Vibration Manager

class VibrationManager {
  constructor() {
    this.isSupported = 'vibrate' in navigator;
    this.isEnabled = true;
    this.patterns = new Map();
    this.activeVibration = null;
    this.initializePatterns();
  }

  // Initialize common patterns
  initializePatterns() {
    // Predefined patterns
    this.patterns.set('success', [50, 100, 50]);
    this.patterns.set('warning', [100, 50, 100]);
    this.patterns.set('error', [200, 100, 200, 100, 200]);
    this.patterns.set('notification', [100]);
    this.patterns.set('tap', [10]);
    this.patterns.set('double-tap', [10, 50, 10]);
    this.patterns.set('long-press', [500]);
    this.patterns.set('pulse', [100, 100, 100, 100, 100]);
    this.patterns.set('heartbeat', [100, 300, 100, 1000]);
    this.patterns.set('sos', [
      100,
      100,
      100,
      100,
      100,
      100, // S
      300,
      100,
      300,
      100,
      300,
      100, // O
      100,
      100,
      100,
      100,
      100,
      100, // S
    ]);
  }

  // Vibrate with pattern
  vibrate(pattern, options = {}) {
    if (!this.canVibrate()) return false;

    const { repeat = 1, delay = 0, intensity = 1 } = options;

    // Stop any active vibration
    this.stop();

    // Get pattern array
    const patternArray = this.getPattern(pattern);
    if (!patternArray) return false;

    // Apply intensity modifier
    const adjustedPattern = this.adjustIntensity(patternArray, intensity);

    // Create repeated pattern if needed
    const finalPattern = this.createRepeatedPattern(
      adjustedPattern,
      repeat,
      delay
    );

    // Execute vibration
    const success = navigator.vibrate(finalPattern);

    if (success) {
      this.activeVibration = {
        pattern: pattern,
        startTime: Date.now(),
        duration: this.calculateDuration(finalPattern),
      };

      // Auto-stop after pattern completes
      this.scheduleStop(this.activeVibration.duration);
    }

    return success;
  }

  // Get pattern array
  getPattern(pattern) {
    if (typeof pattern === 'number') {
      return [pattern];
    } else if (Array.isArray(pattern)) {
      return pattern;
    } else if (typeof pattern === 'string' && this.patterns.has(pattern)) {
      return [...this.patterns.get(pattern)];
    }

    return null;
  }

  // Adjust pattern intensity
  adjustIntensity(pattern, intensity) {
    if (intensity === 1) return pattern;

    return pattern.map((duration, index) => {
      // Only adjust vibration durations (even indices)
      if (index % 2 === 0) {
        return Math.round(duration * intensity);
      }
      return duration;
    });
  }

  // Create repeated pattern
  createRepeatedPattern(pattern, repeat, delay) {
    if (repeat === 1) return pattern;

    const result = [];

    for (let i = 0; i < repeat; i++) {
      if (i > 0 && delay > 0) {
        result.push(delay);
      }
      result.push(...pattern);
    }

    return result;
  }

  // Calculate total duration
  calculateDuration(pattern) {
    return pattern.reduce((sum, duration) => sum + duration, 0);
  }

  // Schedule automatic stop
  scheduleStop(duration) {
    if (this.stopTimeout) {
      clearTimeout(this.stopTimeout);
    }

    this.stopTimeout = setTimeout(() => {
      this.activeVibration = null;
    }, duration);
  }

  // Stop vibration
  stop() {
    if (this.stopTimeout) {
      clearTimeout(this.stopTimeout);
      this.stopTimeout = null;
    }

    this.activeVibration = null;
    return navigator.vibrate(0);
  }

  // Check if can vibrate
  canVibrate() {
    return this.isSupported && this.isEnabled;
  }

  // Enable/disable vibration
  setEnabled(enabled) {
    this.isEnabled = enabled;

    if (!enabled) {
      this.stop();
    }
  }

  // Add custom pattern
  addPattern(name, pattern) {
    this.patterns.set(name, pattern);
  }

  // Remove pattern
  removePattern(name) {
    return this.patterns.delete(name);
  }

  // Get all patterns
  getPatterns() {
    return Array.from(this.patterns.keys());
  }

  // Create pattern from morse code
  createMorsePattern(text, unitDuration = 100) {
    const morseCode = {
      A: '.-',
      B: '-...',
      C: '-.-.',
      D: '-..',
      E: '.',
      F: '..-.',
      G: '--.',
      H: '....',
      I: '..',
      J: '.---',
      K: '-.-',
      L: '.-..',
      M: '--',
      N: '-.',
      O: '---',
      P: '.--.',
      Q: '--.-',
      R: '.-.',
      S: '...',
      T: '-',
      U: '..-',
      V: '...-',
      W: '.--',
      X: '-..-',
      Y: '-.--',
      Z: '--..',
      0: '-----',
      1: '.----',
      2: '..---',
      3: '...--',
      4: '....-',
      5: '.....',
      6: '-....',
      7: '--...',
      8: '---..',
      9: '----.',
      ' ': '/',
    };

    const pattern = [];
    const upperText = text.toUpperCase();

    for (const char of upperText) {
      if (morseCode[char]) {
        const morse = morseCode[char];

        for (const symbol of morse) {
          if (symbol === '.') {
            pattern.push(unitDuration); // Dot
          } else if (symbol === '-') {
            pattern.push(unitDuration * 3); // Dash
          } else if (symbol === '/') {
            pattern.push(0, unitDuration * 7, 0); // Word space
            continue;
          }

          pattern.push(unitDuration); // Symbol space
        }

        pattern.push(unitDuration * 2); // Letter space (total 3 units)
      }
    }

    // Remove trailing pause
    if (pattern.length > 0) {
      pattern.pop();
    }

    return pattern;
  }

  // Create rhythmic pattern
  createRhythmPattern(beats, tempo = 120) {
    const beatDuration = 60000 / tempo; // ms per beat
    const pattern = [];

    for (const beat of beats) {
      if (beat === '1') {
        pattern.push(50); // Vibrate
      }
      pattern.push(beatDuration - 50); // Pause
    }

    return pattern;
  }
}

// Usage
const vibration = new VibrationManager();

// Simple vibration
vibration.vibrate(100);

// Use predefined pattern
vibration.vibrate('success');

// Custom pattern with options
vibration.vibrate('notification', {
  repeat: 3,
  delay: 200,
  intensity: 0.5,
});

// Create morse code vibration
const morsePattern = vibration.createMorsePattern('SOS');
vibration.vibrate(morsePattern);

// Create rhythm pattern
const rhythm = vibration.createRhythmPattern('1001011010110110');
vibration.vibrate(rhythm);

Interactive Haptic Feedback

Touch Interaction Feedback

class HapticFeedback {
  constructor(options = {}) {
    this.vibration = new VibrationManager();
    this.options = {
      enableForButtons: true,
      enableForInputs: true,
      enableForGestures: true,
      enableForNavigation: true,
      ...options,
    };

    this.gestureDetector = null;
    this.init();
  }

  // Initialize haptic feedback
  init() {
    if (this.options.enableForButtons) {
      this.setupButtonFeedback();
    }

    if (this.options.enableForInputs) {
      this.setupInputFeedback();
    }

    if (this.options.enableForGestures) {
      this.setupGestureFeedback();
    }

    if (this.options.enableForNavigation) {
      this.setupNavigationFeedback();
    }

    this.setupCustomElements();
  }

  // Setup button feedback
  setupButtonFeedback() {
    // Delegate events for better performance
    document.addEventListener('touchstart', (event) => {
      const button = event.target.closest(
        'button, .haptic-button, [role="button"]'
      );
      if (button && !button.disabled) {
        this.handleButtonTouch(button);
      }
    });

    // Long press detection
    let longPressTimer;

    document.addEventListener('touchstart', (event) => {
      const button = event.target.closest('[data-haptic-long-press]');
      if (button) {
        longPressTimer = setTimeout(() => {
          this.vibration.vibrate('long-press');
          button.dispatchEvent(new CustomEvent('hapticlongpress'));
        }, 500);
      }
    });

    document.addEventListener('touchend', () => {
      clearTimeout(longPressTimer);
    });
  }

  // Handle button touch
  handleButtonTouch(button) {
    const hapticType = button.dataset.haptic || 'tap';
    const hapticIntensity = parseFloat(button.dataset.hapticIntensity) || 1;

    this.vibration.vibrate(hapticType, { intensity: hapticIntensity });
  }

  // Setup input feedback
  setupInputFeedback() {
    // Text input feedback
    document.addEventListener('input', (event) => {
      const input = event.target;
      if (input.matches('input[type="text"], input[type="number"], textarea')) {
        if (input.dataset.haptic !== 'false') {
          this.vibration.vibrate(5); // Subtle feedback
        }
      }
    });

    // Checkbox/Radio feedback
    document.addEventListener('change', (event) => {
      const input = event.target;
      if (input.matches('input[type="checkbox"], input[type="radio"]')) {
        this.vibration.vibrate(input.checked ? 'success' : 'tap');
      }
    });

    // Range slider feedback
    let lastSliderValue = new Map();

    document.addEventListener('input', (event) => {
      const slider = event.target;
      if (slider.matches('input[type="range"]')) {
        const value = parseInt(slider.value);
        const lastValue = lastSliderValue.get(slider) || value;

        // Vibrate on step changes
        if (value !== lastValue) {
          const step = parseInt(slider.step) || 1;
          if ((value - lastValue) % step === 0) {
            this.vibration.vibrate(10);
          }
        }

        lastSliderValue.set(slider, value);
      }
    });
  }

  // Setup gesture feedback
  setupGestureFeedback() {
    this.gestureDetector = new GestureDetector();

    // Swipe feedback
    this.gestureDetector.on('swipe', (event) => {
      const element = event.target.closest('[data-haptic-swipe]');
      if (element) {
        this.vibration.vibrate([20, 30, 20]);
      }
    });

    // Pinch feedback
    this.gestureDetector.on('pinch', (event) => {
      const element = event.target.closest('[data-haptic-pinch]');
      if (element) {
        const intensity = Math.min(event.scale, 2) / 2;
        this.vibration.vibrate(30, { intensity });
      }
    });

    // Double tap feedback
    this.gestureDetector.on('doubletap', (event) => {
      const element = event.target.closest('[data-haptic-double-tap]');
      if (element) {
        this.vibration.vibrate('double-tap');
      }
    });
  }

  // Setup navigation feedback
  setupNavigationFeedback() {
    // Page navigation
    window.addEventListener('popstate', () => {
      this.vibration.vibrate([10, 20, 10]);
    });

    // Tab navigation
    document.addEventListener('keydown', (event) => {
      if (event.key === 'Tab') {
        const focusable = event.target.matches(
          'button, a, input, select, textarea, [tabindex]'
        );

        if (focusable) {
          this.vibration.vibrate(5);
        }
      }
    });

    // Scroll feedback
    let scrollEndTimer;
    let lastScrollPosition = 0;

    window.addEventListener('scroll', () => {
      const currentPosition = window.pageYOffset;

      // Detect reaching top/bottom
      if (currentPosition === 0 && lastScrollPosition > 0) {
        this.vibration.vibrate([20, 10, 20]); // Reached top
      } else if (
        window.innerHeight + currentPosition >=
        document.body.offsetHeight
      ) {
        this.vibration.vibrate([20, 10, 20]); // Reached bottom
      }

      lastScrollPosition = currentPosition;

      // Clear existing timer
      clearTimeout(scrollEndTimer);

      // Detect scroll end
      scrollEndTimer = setTimeout(() => {
        const snapElement = document.elementFromPoint(
          window.innerWidth / 2,
          window.innerHeight / 2
        );

        if (snapElement?.dataset.hapticSnap) {
          this.vibration.vibrate('tap');
        }
      }, 150);
    });
  }

  // Setup custom elements
  setupCustomElements() {
    // Success/Error notifications
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === 1) {
            // Element node
            if (node.classList.contains('notification-success')) {
              this.vibration.vibrate('success');
            } else if (node.classList.contains('notification-error')) {
              this.vibration.vibrate('error');
            } else if (node.classList.contains('notification-warning')) {
              this.vibration.vibrate('warning');
            }
          }
        });
      });
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  // Create haptic pattern
  createCustomPattern(type, duration = 100) {
    switch (type) {
      case 'bounce':
        return [
          duration,
          duration / 2,
          duration / 2,
          duration / 4,
          duration / 4,
        ];

      case 'fade-in':
        return [
          duration * 0.2,
          duration * 0.1,
          duration * 0.4,
          duration * 0.1,
          duration * 0.6,
          duration * 0.1,
          duration * 0.8,
          duration * 0.1,
          duration,
        ];

      case 'fade-out':
        return [
          duration,
          duration * 0.1,
          duration * 0.8,
          duration * 0.1,
          duration * 0.6,
          duration * 0.1,
          duration * 0.4,
          duration * 0.1,
          duration * 0.2,
        ];

      default:
        return [duration];
    }
  }

  // Enable/disable haptic feedback
  setEnabled(enabled) {
    this.vibration.setEnabled(enabled);
  }
}

// Simple Gesture Detector
class GestureDetector {
  constructor() {
    this.callbacks = new Map();
    this.touchData = {
      startX: 0,
      startY: 0,
      startTime: 0,
      lastTap: 0,
    };

    this.init();
  }

  init() {
    document.addEventListener('touchstart', this.handleTouchStart.bind(this));
    document.addEventListener('touchend', this.handleTouchEnd.bind(this));
    document.addEventListener('touchmove', this.handleTouchMove.bind(this));
  }

  handleTouchStart(event) {
    const touch = event.touches[0];
    this.touchData.startX = touch.clientX;
    this.touchData.startY = touch.clientY;
    this.touchData.startTime = Date.now();
  }

  handleTouchMove(event) {
    if (event.touches.length === 2) {
      // Pinch detection
      const touch1 = event.touches[0];
      const touch2 = event.touches[1];

      const distance = Math.hypot(
        touch2.clientX - touch1.clientX,
        touch2.clientY - touch1.clientY
      );

      this.emit('pinch', {
        target: event.target,
        scale: distance / 100,
      });
    }
  }

  handleTouchEnd(event) {
    const touch = event.changedTouches[0];
    const deltaX = touch.clientX - this.touchData.startX;
    const deltaY = touch.clientY - this.touchData.startY;
    const deltaTime = Date.now() - this.touchData.startTime;

    // Swipe detection
    if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) {
      if (deltaTime < 300) {
        this.emit('swipe', {
          target: event.target,
          direction: this.getSwipeDirection(deltaX, deltaY),
          distance: Math.hypot(deltaX, deltaY),
        });
      }
    }

    // Double tap detection
    const now = Date.now();
    if (now - this.touchData.lastTap < 300) {
      this.emit('doubletap', {
        target: event.target,
      });
    }
    this.touchData.lastTap = now;
  }

  getSwipeDirection(deltaX, deltaY) {
    if (Math.abs(deltaX) > Math.abs(deltaY)) {
      return deltaX > 0 ? 'right' : 'left';
    } else {
      return deltaY > 0 ? 'down' : 'up';
    }
  }

  on(event, callback) {
    if (!this.callbacks.has(event)) {
      this.callbacks.set(event, []);
    }
    this.callbacks.get(event).push(callback);
  }

  emit(event, data) {
    const callbacks = this.callbacks.get(event) || [];
    callbacks.forEach((callback) => callback(data));
  }
}

// Usage
const hapticFeedback = new HapticFeedback({
  enableForButtons: true,
  enableForInputs: true,
  enableForGestures: true,
  enableForNavigation: true,
});

// Custom haptic elements
document.querySelectorAll('[data-haptic]').forEach((element) => {
  element.addEventListener('click', () => {
    const pattern = element.dataset.haptic;
    const intensity = parseFloat(element.dataset.hapticIntensity) || 1;
    vibration.vibrate(pattern, { intensity });
  });
});

Game Haptic Effects

Gaming Vibration Patterns

class GameHaptics {
  constructor() {
    this.vibration = new VibrationManager();
    this.effects = new Map();
    this.initializeEffects();
  }

  // Initialize game effects
  initializeEffects() {
    // Combat effects
    this.effects.set('hit', [50, 50, 100]);
    this.effects.set('critical-hit', [100, 50, 100, 50, 200]);
    this.effects.set('block', [30, 20, 30]);
    this.effects.set('damage', [200, 100, 150]);
    this.effects.set('death', [500, 200, 300, 200, 100]);

    // Movement effects
    this.effects.set('jump', [50]);
    this.effects.set('land', [100]);
    this.effects.set('footstep', [20]);
    this.effects.set('dash', [30, 10, 30]);

    // Pickup effects
    this.effects.set('coin', [20, 10, 20]);
    this.effects.set('powerup', [50, 50, 100, 50, 150]);
    this.effects.set('health', [100, 100, 100]);

    // Environment effects
    this.effects.set('explosion', [300, 100, 200, 100, 100]);
    this.effects.set('earthquake', this.createEarthquakePattern());
    this.effects.set('thunder', [500, 200, 300]);

    // UI effects
    this.effects.set('level-up', [100, 100, 200, 100, 300]);
    this.effects.set('achievement', [50, 50, 50, 50, 200]);
    this.effects.set('countdown', [100, 900, 100, 900, 100, 900, 500]);
  }

  // Create earthquake pattern
  createEarthquakePattern() {
    const pattern = [];
    const duration = 3000; // 3 seconds
    const intervals = 30;

    for (let i = 0; i < intervals; i++) {
      const intensity = Math.sin((i / intervals) * Math.PI) * 100;
      pattern.push(Math.round(intensity), 50);
    }

    return pattern;
  }

  // Play effect
  playEffect(effectName, options = {}) {
    const pattern = this.effects.get(effectName);
    if (!pattern) {
      console.warn(`Unknown effect: ${effectName}`);
      return;
    }

    const { intensity = 1, delay = 0, priority = 'normal' } = options;

    // Handle priority
    if (priority === 'high' || !this.vibration.activeVibration) {
      setTimeout(() => {
        this.vibration.vibrate(pattern, { intensity });
      }, delay);
    }
  }

  // Play combo effect
  playCombo(effects, interval = 100) {
    let delay = 0;

    effects.forEach((effect) => {
      this.playEffect(effect, { delay });

      const pattern = this.effects.get(effect);
      if (pattern) {
        delay += this.vibration.calculateDuration(pattern) + interval;
      }
    });
  }

  // Create custom explosion
  createExplosion(intensity = 1, duration = 500) {
    const segments = 5;
    const pattern = [];

    for (let i = 0; i < segments; i++) {
      const segmentIntensity = intensity * (1 - i / segments);
      const vibrationTime = Math.round(
        (duration / segments) * segmentIntensity
      );
      const pauseTime = Math.round(
        (duration / segments) * (1 - segmentIntensity)
      );

      pattern.push(vibrationTime, pauseTime);
    }

    return pattern;
  }

  // Create machine gun effect
  createMachineGun(rounds = 10, rpm = 600) {
    const interval = 60000 / rpm;
    const pattern = [];

    for (let i = 0; i < rounds; i++) {
      pattern.push(20, interval - 20);
    }

    return pattern;
  }

  // Create engine vibration
  createEngine(rpm = 3000, duration = 2000) {
    const frequency = rpm / 60;
    const pulseInterval = 1000 / frequency;
    const pulses = Math.floor(duration / pulseInterval);
    const pattern = [];

    for (let i = 0; i < pulses; i++) {
      pattern.push(10, pulseInterval - 10);
    }

    return pattern;
  }

  // Collision feedback
  handleCollision(force = 1, angle = 0) {
    // Calculate vibration based on collision force
    const baseIntensity = Math.min(force, 1);
    const duration = 50 + force * 150;

    // Create directional feedback (if device supports)
    const pattern = [duration];

    this.vibration.vibrate(pattern, { intensity: baseIntensity });
  }

  // Continuous effects
  startContinuousEffect(effectName, interval = 1000) {
    this.stopContinuousEffect(effectName);

    const pattern = this.effects.get(effectName);
    if (!pattern) return;

    this.continuousEffects = this.continuousEffects || new Map();

    const playEffect = () => {
      this.vibration.vibrate(pattern);
    };

    playEffect();
    const intervalId = setInterval(playEffect, interval);

    this.continuousEffects.set(effectName, intervalId);
  }

  // Stop continuous effect
  stopContinuousEffect(effectName) {
    if (this.continuousEffects && this.continuousEffects.has(effectName)) {
      clearInterval(this.continuousEffects.get(effectName));
      this.continuousEffects.delete(effectName);
    }
  }

  // Stop all effects
  stopAll() {
    this.vibration.stop();

    if (this.continuousEffects) {
      this.continuousEffects.forEach((intervalId) => clearInterval(intervalId));
      this.continuousEffects.clear();
    }
  }
}

// Game implementation example
class HapticGame {
  constructor() {
    this.haptics = new GameHaptics();
    this.score = 0;
    this.combo = 0;
  }

  // Player actions
  playerJump() {
    this.haptics.playEffect('jump');
  }

  playerLand() {
    this.haptics.playEffect('land');
  }

  playerHit(damage) {
    if (damage > 50) {
      this.haptics.playEffect('critical-hit');
    } else {
      this.haptics.playEffect('hit');
    }
  }

  playerDeath() {
    this.haptics.playEffect('death', { intensity: 1, priority: 'high' });
  }

  // Collectibles
  collectCoin() {
    this.score += 10;
    this.haptics.playEffect('coin');

    // Combo feedback
    this.combo++;
    if (this.combo > 5) {
      this.haptics.playEffect('achievement', { delay: 100 });
    }
  }

  collectPowerup(type) {
    this.haptics.playEffect('powerup');

    // Start continuous effect for powerup duration
    if (type === 'speed') {
      this.haptics.startContinuousEffect('footstep', 200);

      setTimeout(() => {
        this.haptics.stopContinuousEffect('footstep');
      }, 10000); // 10 second powerup
    }
  }

  // Environmental
  triggerExplosion(distance) {
    const intensity = Math.max(0, 1 - distance / 100);
    const customExplosion = this.haptics.createExplosion(intensity);
    this.haptics.vibration.vibrate(customExplosion);
  }

  startEarthquake() {
    this.haptics.playEffect('earthquake', { priority: 'high' });
  }

  // Weapons
  fireMachineGun() {
    const pattern = this.haptics.createMachineGun(20, 800);
    this.haptics.vibration.vibrate(pattern);
  }

  // Vehicle
  startEngine() {
    const pattern = this.haptics.createEngine(4000, 1000);
    this.haptics.vibration.vibrate(pattern);
  }
}

// Usage
const game = new HapticGame();

// Game events
game.playerJump();
setTimeout(() => game.playerLand(), 500);

// Combat
game.playerHit(30);
game.collectPowerup('speed');

// Environmental effects
game.triggerExplosion(50); // 50 units away

Accessibility and User Preferences

Haptic Settings Manager

class HapticSettings {
  constructor() {
    this.settings = this.loadSettings();
    this.vibration = new VibrationManager();
    this.initializeUI();
  }

  // Default settings
  getDefaultSettings() {
    return {
      enabled: true,
      intensity: 1,
      patterns: {
        notifications: true,
        interactions: true,
        navigation: true,
        games: true,
      },
      customPatterns: {},
      reducedMotion: false,
      batteryOnly: false,
    };
  }

  // Load settings
  loadSettings() {
    try {
      const saved = localStorage.getItem('hapticSettings');
      return saved ? JSON.parse(saved) : this.getDefaultSettings();
    } catch (error) {
      return this.getDefaultSettings();
    }
  }

  // Save settings
  saveSettings() {
    try {
      localStorage.setItem('hapticSettings', JSON.stringify(this.settings));
      this.applySettings();
    } catch (error) {
      console.error('Failed to save haptic settings:', error);
    }
  }

  // Apply settings
  applySettings() {
    this.vibration.setEnabled(this.settings.enabled);

    // Check reduced motion preference
    if (this.settings.reducedMotion || this.prefersReducedMotion()) {
      this.settings.intensity *= 0.5;
    }

    // Check battery preference
    if (this.settings.batteryOnly) {
      navigator.getBattery?.().then((battery) => {
        if (!battery.charging && battery.level < 0.2) {
          this.vibration.setEnabled(false);
        }
      });
    }
  }

  // Check system reduced motion preference
  prefersReducedMotion() {
    return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  }

  // Initialize settings UI
  initializeUI() {
    const container = document.createElement('div');
    container.className = 'haptic-settings';
    container.innerHTML = `
      <h3>Haptic Feedback Settings</h3>
      
      <div class="setting-group">
        <label>
          <input type="checkbox" id="haptic-enabled" ${this.settings.enabled ? 'checked' : ''}>
          <span>Enable Haptic Feedback</span>
        </label>
      </div>
      
      <div class="setting-group">
        <label>
          <span>Intensity</span>
          <input type="range" id="haptic-intensity" 
                 min="0" max="1" step="0.1" 
                 value="${this.settings.intensity}">
          <span class="intensity-value">${Math.round(this.settings.intensity * 100)}%</span>
        </label>
      </div>
      
      <div class="setting-group">
        <h4>Feedback Types</h4>
        ${Object.entries(this.settings.patterns)
          .map(
            ([key, value]) => `
          <label>
            <input type="checkbox" 
                   data-pattern="${key}" 
                   ${value ? 'checked' : ''}>
            <span>${this.formatLabel(key)}</span>
          </label>
        `
          )
          .join('')}
      </div>
      
      <div class="setting-group">
        <label>
          <input type="checkbox" id="haptic-reduced" ${this.settings.reducedMotion ? 'checked' : ''}>
          <span>Reduce haptic intensity</span>
        </label>
      </div>
      
      <div class="setting-group">
        <label>
          <input type="checkbox" id="haptic-battery" ${this.settings.batteryOnly ? 'checked' : ''}>
          <span>Disable on low battery</span>
        </label>
      </div>
      
      <div class="setting-group">
        <button id="haptic-test">Test Vibration</button>
        <button id="haptic-reset">Reset to Defaults</button>
      </div>
    `;

    // Add styles
    const style = document.createElement('style');
    style.textContent = `
      .haptic-settings {
        padding: 20px;
        background: #f5f5f5;
        border-radius: 8px;
        max-width: 400px;
        font-family: system-ui, -apple-system, sans-serif;
      }

      .haptic-settings h3 {
        margin-top: 0;
      }

      .setting-group {
        margin: 15px 0;
      }

      .setting-group label {
        display: flex;
        align-items: center;
        gap: 10px;
        margin: 8px 0;
        cursor: pointer;
      }

      .setting-group input[type="checkbox"] {
        width: 20px;
        height: 20px;
      }

      .setting-group input[type="range"] {
        flex: 1;
        margin: 0 10px;
      }

      .intensity-value {
        min-width: 40px;
        text-align: right;
      }

      .setting-group button {
        margin: 5px;
        padding: 8px 16px;
        border: none;
        background: #007bff;
        color: white;
        border-radius: 4px;
        cursor: pointer;
      }

      .setting-group button:hover {
        background: #0056b3;
      }
    `;

    document.head.appendChild(style);

    // Add to page
    const settingsContainer = document.getElementById(
      'haptic-settings-container'
    );
    if (settingsContainer) {
      settingsContainer.appendChild(container);
    } else {
      document.body.appendChild(container);
    }

    // Attach event listeners
    this.attachSettingsListeners(container);
  }

  // Attach settings listeners
  attachSettingsListeners(container) {
    // Enable/disable
    container
      .querySelector('#haptic-enabled')
      .addEventListener('change', (e) => {
        this.settings.enabled = e.target.checked;
        this.saveSettings();
      });

    // Intensity
    const intensitySlider = container.querySelector('#haptic-intensity');
    const intensityValue = container.querySelector('.intensity-value');

    intensitySlider.addEventListener('input', (e) => {
      const value = parseFloat(e.target.value);
      this.settings.intensity = value;
      intensityValue.textContent = `${Math.round(value * 100)}%`;
      this.saveSettings();

      // Test vibration with new intensity
      this.vibration.vibrate(50, { intensity: value });
    });

    // Pattern toggles
    container.querySelectorAll('[data-pattern]').forEach((checkbox) => {
      checkbox.addEventListener('change', (e) => {
        const pattern = e.target.dataset.pattern;
        this.settings.patterns[pattern] = e.target.checked;
        this.saveSettings();
      });
    });

    // Reduced motion
    container
      .querySelector('#haptic-reduced')
      .addEventListener('change', (e) => {
        this.settings.reducedMotion = e.target.checked;
        this.saveSettings();
      });

    // Battery only
    container
      .querySelector('#haptic-battery')
      .addEventListener('change', (e) => {
        this.settings.batteryOnly = e.target.checked;
        this.saveSettings();
      });

    // Test button
    container.querySelector('#haptic-test').addEventListener('click', () => {
      this.testVibration();
    });

    // Reset button
    container.querySelector('#haptic-reset').addEventListener('click', () => {
      this.resetSettings();
      location.reload(); // Refresh UI
    });
  }

  // Format label
  formatLabel(key) {
    return key
      .split(/(?=[A-Z])/)
      .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
      .join(' ');
  }

  // Test vibration
  testVibration() {
    if (!this.settings.enabled) {
      alert('Haptic feedback is disabled');
      return;
    }

    const testPattern = [100, 100, 100, 100, 300];
    this.vibration.vibrate(testPattern, {
      intensity: this.settings.intensity,
    });
  }

  // Reset settings
  resetSettings() {
    this.settings = this.getDefaultSettings();
    this.saveSettings();
  }

  // Check if pattern is enabled
  isPatternEnabled(category) {
    return this.settings.enabled && this.settings.patterns[category];
  }

  // Get adjusted intensity
  getAdjustedIntensity() {
    let intensity = this.settings.intensity;

    if (this.settings.reducedMotion || this.prefersReducedMotion()) {
      intensity *= 0.5;
    }

    return intensity;
  }
}

// Usage
const hapticSettings = new HapticSettings();

// Check before vibrating
function vibrateSafely(pattern, category = 'interactions') {
  if (hapticSettings.isPatternEnabled(category)) {
    const intensity = hapticSettings.getAdjustedIntensity();
    vibration.vibrate(pattern, { intensity });
  }
}

Best Practices

  1. Always check for support

    if ('vibrate' in navigator) {
      navigator.vibrate(100);
    }
    
  2. Provide user control

    // Always allow users to disable vibration
    const enableVibration = localStorage.getItem('vibration') !== 'false';
    
  3. Use appropriate patterns

    // Keep vibrations short and meaningful
    const subtle = [10];
    const moderate = [50];
    const strong = [100];
    
  4. Consider battery impact

    // Reduce vibration on low battery
    if (battery.level < 0.2) {
      reduceVibrationIntensity();
    }
    

Browser Compatibility

The Vibration API is primarily supported on mobile devices:

class VibrationPolyfill {
  constructor() {
    this.supported = 'vibrate' in navigator;

    if (!this.supported) {
      // Provide visual feedback as fallback
      this.setupVisualFallback();
    }
  }

  setupVisualFallback() {
    navigator.vibrate = (pattern) => {
      // Flash screen or show visual indicator
      const flash = document.createElement('div');
      flash.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(255, 255, 255, 0.1);
        pointer-events: none;
        z-index: 99999;
      `;

      document.body.appendChild(flash);

      setTimeout(
        () => {
          flash.remove();
        },
        Array.isArray(pattern) ? pattern[0] : pattern
      );

      return true;
    };
  }
}

// Initialize polyfill
new VibrationPolyfill();

Conclusion

The Vibration API enhances user experience through tactile feedback:

  • Improved interaction with haptic responses
  • Better accessibility for users with visual impairments
  • Enhanced gaming with immersive effects
  • Subtle notifications without sound
  • Confirmation feedback for user actions
  • Navigation assistance with haptic cues

Key takeaways:

  • Always check for API support
  • Provide user controls for vibration
  • Keep patterns short and meaningful
  • Consider battery and accessibility
  • Test on real devices
  • Provide fallbacks for unsupported devices

Create more engaging and accessible web experiences with thoughtful haptic feedback!