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.
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
-
Always check for support
if ('vibrate' in navigator) { navigator.vibrate(100); }
-
Provide user control
// Always allow users to disable vibration const enableVibration = localStorage.getItem('vibration') !== 'false';
-
Use appropriate patterns
// Keep vibrations short and meaningful const subtle = [10]; const moderate = [50]; const strong = [100];
-
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!