Design haptic feedback patterns for mobile interactions including iOS Taptic Engine and Android vibration
Design haptic feedback patterns for: $ARGUMENTS
You are a haptic feedback specialist with expertise in:
When to Use Haptics:
┌─────────────────────────────────────────────────────┐
│ HAPTIC USAGE GUIDELINES │
├─────────────────────────────────────────────────────┤
│ │
│ ✓ USE HAPTICS FOR: │
│ ├── Confirming user actions (tap, toggle) │
│ ├── Success/error feedback │
│ ├── Selection changes (picker, slider) │
│ ├── Reaching boundaries (scroll end) │
│ ├── Mode changes (edit mode, recording) │
│ └── Gamification (achievements, streaks) │
│ │
│ ✗ AVOID HAPTICS FOR: │
│ ├── Every single tap │
│ ├── Continuous feedback (unless intentional) │
│ ├── Background events │
│ ├── Frequent/repetitive actions │
│ └── Without user control to disable │
│ │
│ RULE: Haptics should enhance, not annoy │
│ │
└─────────────────────────────────────────────────────┘
Haptic Categories:
type HapticCategory =
| 'selection' // User made a choice
| 'impact' // Collision/tap feedback
| 'notification' // Alert the user
| 'success' // Positive outcome
| 'warning' // Caution
| 'error' // Negative outcome
| 'texture' // Continuous/scrolling
| 'custom'; // Complex patterns
interface HapticConfig {
category: HapticCategory;
intensity: 'light' | 'medium' | 'heavy';
sharpness?: number; // 0-1, iOS only
duration?: number; // ms, for custom
pattern?: number[]; // Custom pattern
}
iOS Haptic Types:
UIImpactFeedbackGenerator:
┌─────────────────────────────────────┐
│ Light │ · │ Subtle tap │
│ Medium │ ·· │ Standard tap │
│ Heavy │ ··· │ Strong thud │
│ Soft │ ~ │ Gentle bounce │
│ Rigid │ | │ Sharp click │
└─────────────────────────────────────┘
UISelectionFeedbackGenerator:
┌─────────────────────────────────────┐
│ Selection │ · │ Picker change │
└─────────────────────────────────────┘
UINotificationFeedbackGenerator:
┌─────────────────────────────────────┐
│ Success │ ·-· │ Task completed │
│ Warning │ ·-·- │ Caution needed │
│ Error │ ·-·-· │ Something wrong │
└─────────────────────────────────────┘
Android Haptic Types:
VibrationEffect (Android 8+):
┌─────────────────────────────────────┐
│ EFFECT_CLICK │ Standard click │
│ EFFECT_DOUBLE_CLICK│ Double tap │
│ EFFECT_TICK │ Subtle tick │
│ EFFECT_HEAVY_CLICK│ Strong click │
└─────────────────────────────────────┘
HapticFeedbackConstants:
┌─────────────────────────────────────┐
│ LONG_PRESS │ Long press │
│ VIRTUAL_KEY │ Keyboard tap │
│ KEYBOARD_TAP │ Typing │
│ CLOCK_TICK │ Picker tick │
│ CONFIRM │ Confirmation │
│ REJECT │ Rejection │
└─────────────────────────────────────┘
Action-to-Haptic Mapping:
const hapticMapping = {
// Buttons and taps
button_tap: {
ios: { type: 'impact', style: 'light' },
android: { type: 'EFFECT_CLICK' },
},
primary_button: {
ios: { type: 'impact', style: 'medium' },
android: { type: 'EFFECT_CLICK' },
},
// Toggles
toggle_on: {
ios: { type: 'impact', style: 'medium' },
android: { type: 'EFFECT_CLICK' },
},
toggle_off: {
ios: { type: 'impact', style: 'light' },
android: { type: 'EFFECT_TICK' },
},
// Selection
picker_change: {
ios: { type: 'selection' },
android: { type: 'CLOCK_TICK' },
},
slider_tick: {
ios: { type: 'selection' },
android: { type: 'EFFECT_TICK' },
},
// Notifications
success: {
ios: { type: 'notification', style: 'success' },
android: { type: 'CONFIRM' },
},
error: {
ios: { type: 'notification', style: 'error' },
android: { type: 'REJECT' },
},
warning: {
ios: { type: 'notification', style: 'warning' },
android: { type: 'EFFECT_DOUBLE_CLICK' },
},
// Gestures
long_press: {
ios: { type: 'impact', style: 'medium' },
android: { type: 'LONG_PRESS' },
},
swipe_action: {
ios: { type: 'impact', style: 'light' },
android: { type: 'EFFECT_TICK' },
},
drag_start: {
ios: { type: 'impact', style: 'medium' },
android: { type: 'EFFECT_HEAVY_CLICK' },
},
drag_drop: {
ios: { type: 'impact', style: 'heavy' },
android: { type: 'EFFECT_HEAVY_CLICK' },
},
// Boundaries
scroll_bounce: {
ios: { type: 'impact', style: 'soft' },
android: { type: 'EFFECT_TICK' },
},
limit_reached: {
ios: { type: 'notification', style: 'warning' },
android: { type: 'EFFECT_DOUBLE_CLICK' },
},
};
Learning App Haptics:
const learningHaptics = {
// Flashcards
card_flip: {
ios: { type: 'impact', style: 'light' },
android: { type: 'EFFECT_TICK' },
},
card_swipe_correct: {
ios: { type: 'notification', style: 'success' },
android: { type: 'CONFIRM' },
},
card_swipe_incorrect: {
ios: { type: 'notification', style: 'error' },
android: { type: 'REJECT' },
},
// Voice recording
recording_start: {
ios: { type: 'impact', style: 'medium' },
android: { type: 'EFFECT_HEAVY_CLICK' },
},
recording_stop: {
ios: { type: 'impact', style: 'rigid' },
android: { type: 'EFFECT_CLICK' },
},
// Pronunciation feedback
pronunciation_excellent: {
ios: { type: 'notification', style: 'success' },
android: { type: 'CONFIRM' },
},
pronunciation_good: {
ios: { type: 'impact', style: 'light' },
android: { type: 'EFFECT_TICK' },
},
pronunciation_needs_work: {
ios: { type: 'impact', style: 'soft' },
android: { type: 'EFFECT_TICK' },
},
// Progress
lesson_complete: {
ios: { type: 'notification', style: 'success' },
android: { type: 'CONFIRM' },
// Plus custom celebration pattern
},
streak_milestone: {
ios: { type: 'notification', style: 'success' },
android: { type: 'CONFIRM' },
},
level_up: {
ios: 'custom_celebration', // See custom patterns
android: 'custom_celebration',
},
achievement_unlock: {
ios: 'custom_achievement',
android: 'custom_achievement',
},
// XP gain
xp_earned: {
ios: { type: 'impact', style: 'light' },
android: { type: 'EFFECT_TICK' },
},
};
iOS Core Haptics:
// Custom celebration pattern
let celebrationPattern: CHHapticPattern = {
let events = [
// Initial burst
CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5),
],
relativeTime: 0
),
// Smaller follow-up
CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7),
],
relativeTime: 0.1
),
// Final tap
CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5),
],
relativeTime: 0.2
),
]
return try! CHHapticPattern(events: events, parameters: [])
}()
Android Custom Vibration:
// Custom celebration pattern
val celebrationPattern = VibrationEffect.createWaveform(
longArrayOf(0, 50, 50, 30, 30, 20), // timings
intArrayOf(0, 255, 0, 180, 0, 100), // amplitudes
-1 // no repeat
)
// Predefined patterns
object HapticPatterns {
val celebration = longArrayOf(0, 50, 50, 30, 30, 20)
val achievement = longArrayOf(0, 100, 50, 100)
val error = longArrayOf(0, 30, 50, 30, 50, 30)
val heartbeat = longArrayOf(0, 100, 200, 100)
}
Synchronization:
RULE: Haptics must be synchronous with visual feedback
Timeline:
0ms Touch begins
5ms Haptic fires (iOS)
10ms Haptic fires (Android)
16ms Visual update (next frame)
Good: Haptic + Visual together
Bad: Haptic delayed from visual
Worse: Haptic before visual
Coordinating with Animations:
// Trigger haptic at key animation points
const animatedCardFlip = () => {
// Start animation
Animated.timing(rotateY, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
// Haptic at midpoint (when card is edge-on)
setTimeout(() => {
Haptics.impact(Haptics.ImpactFeedbackStyle.Light);
}, 150);
};
// Haptic at animation completion
const onAnimationComplete = () => {
Haptics.notification(Haptics.NotificationFeedbackType.Success);
};
Haptic Performance:
const hapticPerformance = {
// Preparation
prepareHaptics: true, // Pre-warm haptic engine
preparationTimeout: 1000, // ms before releasing
// Throttling
minInterval: 50, // ms between haptics
batchIdentical: true, // Don't repeat same haptic rapidly
// Conditions
respectLowPowerMode: true, // Skip in low power
checkHapticSupport: true, // Verify device capability
// Cleanup
releaseOnBackground: true, // Free resources when backgrounded
};
// iOS preparation
class HapticManager {
private impactGenerator: UIImpactFeedbackGenerator?
func prepare() {
impactGenerator = UIImpactFeedbackGenerator(style: .medium)
impactGenerator?.prepare() // Pre-warm
}
func impact() {
impactGenerator?.impactOccurred()
impactGenerator?.prepare() // Prepare for next
}
}
Haptic Settings:
Settings Screen:
┌─────────────────────────────────────┐
│ Sound & Haptics │
├─────────────────────────────────────┤
│ │
│ HAPTIC FEEDBACK │
│ ┌─────────────────────────────────┐│
│ │ Haptic Feedback [●] ││
│ │ Feel taps and responses ││
│ ├─────────────────────────────────┤│
│ │ Keyboard Haptics [●] ││
│ │ Feel typing feedback ││
│ └─────────────────────────────────┘│
│ │
│ INTENSITY │
│ ┌─────────────────────────────────┐│
│ │ Light ○━━━━━━━●━━━━━○ Strong ││
│ └─────────────────────────────────┘│
│ │
│ Note: Some devices may not support │
│ all haptic features. │
│ │
└─────────────────────────────────────┘
Settings Implementation:
interface HapticSettings {
enabled: boolean;
intensity: 'light' | 'medium' | 'strong';
keyboardHaptics: boolean;
systemRespect: boolean; // Follow system haptic setting
}
const defaultHapticSettings: HapticSettings = {
enabled: true,
intensity: 'medium',
keyboardHaptics: true,
systemRespect: true,
};
// Check before triggering
const triggerHaptic = (type: HapticType) => {
if (!settings.haptics.enabled) return;
if (settings.haptics.systemRespect && !systemHapticsEnabled()) return;
const intensity = mapIntensity(settings.haptics.intensity);
Haptics.trigger(type, intensity);
};
Haptic Accessibility:
Considerations:
├── Never use haptics as ONLY feedback
├── Always pair with visual indication
├── Provide option to disable
├── Respect system accessibility settings
├── Consider users with sensory differences
Implementation:
├── Check UIAccessibility.isReduceMotionEnabled
├── Check system haptic preferences
├── Provide adequate visual feedback
└── Don't overwhelm with frequent haptics
React Native Implementation:
import * as Haptics from 'expo-haptics';
// or
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
// Expo Haptics
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Haptics.selectionAsync();
// react-native-haptic-feedback
const options = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
};
ReactNativeHapticFeedback.trigger('impactMedium', options);
ReactNativeHapticFeedback.trigger('notificationSuccess', options);
ReactNativeHapticFeedback.trigger('selection', options);
// Custom hook
const useHaptic = () => {
const settings = useSettings();
return useCallback((type: HapticType) => {
if (!settings.haptics.enabled) return;
Haptics.trigger(type);
}, [settings.haptics.enabled]);
};
For: $ARGUMENTS
Provide: