Haptic Feedback in .NET MAUI 10: iOS Taptic Engine and Android Vibration Effects (2026)
Build custom haptic feedback in .NET MAUI 10 by calling iOS CoreHaptics and Android VibrationEffect directly. Full platform code, DI wiring, and the API pitfalls that break on iPad, low-power Android, and the iOS Simulator.
Custom haptic feedback in .NET MAUI 10 means bypassing the built-in HapticFeedback API and calling native platform code directly: on iOS you drive Apple's Taptic Engine through CoreHaptics (or the pre-canned UIFeedbackGenerator classes), and on Android you play patterns through VibrationEffect and VibratorManager. The Essentials wrapper only exposes Click and LongPress. Anything richer (texture, ramps, AHAP patterns, predefined Android 12+ effects) needs a platform handler. This guide shows the exact code, the API version pitfalls, and the battery and accessibility rules Apple and Google actually enforce.
The built-in Microsoft.Maui.Devices.HapticFeedback only covers two feedback types; custom haptics need #if IOS/#if ANDROID partial handlers.
On iOS 13+, CoreHaptics plays continuous and transient events with sharpness and intensity parameters; AHAP JSON files are the portable format.
Android 12 (API 31) introduced VibratorManager, deprecating direct Vibrator access, and added predefined effects like EFFECT_TICK and EFFECT_CLICK.
Haptics require the VIBRATE permission on Android and honour the iOS Settings toggle "System Haptics". You cannot override the user's choice.
Repeated haptic calls in tight loops throttle on iOS and drain battery on Android; debounce to at least 40 ms between events.
Accessibility guidelines (Apple HIG, Android Material) require haptics to reinforce, not replace, visual or audio feedback.
Why the .NET MAUI HapticFeedback API is limited
The Microsoft.Maui.Devices.HapticFeedback class in .NET MAUI 10 exposes exactly one method, Perform(HapticFeedbackType type), and the enum has two values: Click and LongPress. That's intentional. Microsoft's team chose to expose only the intersection of what both platforms could reproduce reliably without extra permissions, and the mapping is thin. On iOS Click maps to a light UIImpactFeedbackGenerator, and on Android it hits a fixed HapticFeedbackConstants.KEYBOARD_TAP. If your product manager asks for the iMessage "haha" burst, the Twitter pull-to-refresh thump, or a Duolingo streak rumble, you have to go native.
I ran into this on a fitness app last year. The design team spec'd a "heartbeat" pattern that was two soft transients 220 ms apart, and the MAUI wrapper simply cannot express intensity or timing. So we opened partial classes and wrote platform handlers. The pattern I settled on, and which I'll walk through below, is a small IHapticService interface with two platform implementations, registered through the MAUI dependency injection container. It respects the Essentials cross-platform ergonomics without lying about what's available where.
One more thing worth calling out: the Essentials HapticFeedback docs mention the API can throw FeatureNotSupportedException. In practice this fires on iOS Simulator, on some low-end Android emulators, and on Android tablets that ship without a linear resonant actuator. Wrap every call in a try/catch or the app will hard-crash on a Kindle Fire.
iOS Taptic Engine: UIFeedbackGenerator and CoreHaptics
Apple ships two layers of haptic API. The high-level one is UIFeedbackGenerator, which has three concrete subclasses that map to standard interaction feedback. The low-level one is the CoreHaptics framework, added in iOS 13, which lets you author custom patterns using events, parameters, and curves.
UIFeedbackGenerator for standard interactions
For simple taps, use UIImpactFeedbackGenerator with one of the four styles (Light, Medium, Heavy, plus the iOS 13+ additions Soft and Rigid). For success/warning/error feedback, use UINotificationFeedbackGenerator. For continuous scrolling selections (like a picker), use UISelectionFeedbackGenerator. Apple's documented rule is to call Prepare() before you know you'll fire the feedback, so the Taptic Engine spins up its actuator; otherwise you get up to 200 ms of latency on the first hit.
// Platforms/iOS/HapticServiceIos.cs
using CoreHaptics;
using UIKit;
public class HapticServiceIos : IHapticService
{
private CHHapticEngine? _engine;
public void Impact(HapticImpact style)
{
var uiStyle = style switch
{
HapticImpact.Light => UIImpactFeedbackStyle.Light,
HapticImpact.Medium => UIImpactFeedbackStyle.Medium,
HapticImpact.Heavy => UIImpactFeedbackStyle.Heavy,
HapticImpact.Soft => UIImpactFeedbackStyle.Soft,
HapticImpact.Rigid => UIImpactFeedbackStyle.Rigid,
_ => UIImpactFeedbackStyle.Medium
};
using var generator = new UIImpactFeedbackGenerator(uiStyle);
generator.Prepare();
generator.ImpactOccurred();
}
public void Notify(HapticNotification kind)
{
var uiKind = kind switch
{
HapticNotification.Success => UINotificationFeedbackType.Success,
HapticNotification.Warning => UINotificationFeedbackType.Warning,
HapticNotification.Error => UINotificationFeedbackType.Error,
_ => UINotificationFeedbackType.Success
};
using var generator = new UINotificationFeedbackGenerator();
generator.Prepare();
generator.NotificationOccurred(uiKind);
}
}
CoreHaptics for custom patterns
When you need texture, ramps, or timed events, spin up a CHHapticEngine. Two parameter dimensions matter: intensity (0.0–1.0, how strong the buzz feels) and sharpness (0.0–1.0, how crisp vs. muffled). Events come in two flavours, HapticTransient (instantaneous tap) and HapticContinuous (sustained buzz you can modulate). Below is the "heartbeat" pattern from earlier: two soft transients 220 ms apart.
public async Task PlayHeartbeatAsync()
{
if (!CHHapticEngine.SupportsHaptics) return;
_engine ??= new CHHapticEngine(out var initErr);
if (initErr != null) return;
_engine.StoppedHandler = _ => { _engine = null; };
_engine.ResetHandler = () => _engine?.Start(out _);
var events = new[]
{
new CHHapticEvent(CHHapticEventType.HapticTransient,
new[]
{
new CHHapticEventParameter(CHHapticEventParameterId.HapticIntensity, 0.7f),
new CHHapticEventParameter(CHHapticEventParameterId.HapticSharpness, 0.3f)
}, relativeTime: 0.0),
new CHHapticEvent(CHHapticEventType.HapticTransient,
new[]
{
new CHHapticEventParameter(CHHapticEventParameterId.HapticIntensity, 0.9f),
new CHHapticEventParameter(CHHapticEventParameterId.HapticSharpness, 0.5f)
}, relativeTime: 0.22)
};
var pattern = new CHHapticPattern(events, Array.Empty<CHHapticDynamicParameter>(), out var patternErr);
if (patternErr != null) return;
_engine.Start(out _);
var player = _engine.CreatePlayer(pattern, out _);
player?.Start(0, out _);
}
For complex patterns you can also load AHAP (Apple Haptic and Audio Pattern) JSON files bundled in your app resources and hand them to _engine.PlayPatternFromData. AHAP is a documented JSON schema and Apple ships several examples in their WWDC 2019 "Designing Audio-Haptic Experiences" session. Design tools like Lofelt Studio (now part of Meta) can author AHAP visually, which saves you writing intensity curves by hand.
Android VibrationEffect and VibratorManager
Android's story is messier because the platform gained precise haptic controls only recently. The old Vibrator service accepted a millisecond duration; that was it. Android 8.0 (API 26) introduced VibrationEffect with amplitude control and the ability to pass waveform arrays. Android 10 (API 29) added predefined effects (EFFECT_CLICK, EFFECT_DOUBLE_CLICK, EFFECT_HEAVY_CLICK, EFFECT_TICK). Android 12 (API 31) added VibratorManager, which supports multi-vibrator devices like the Pixel 6 Pro and deprecates direct Vibrator access.
Your MAUI 10 SupportedOSPlatformVersion is Android 21 by default, but the interesting APIs live on 26+, so version guards matter. Here's a service that handles all three tiers.
// Platforms/Android/HapticServiceAndroid.cs
using Android.Content;
using Android.OS;
using Application = Android.App.Application;
public class HapticServiceAndroid : IHapticService
{
private static Vibrator? GetVibrator()
{
var ctx = Application.Context;
if (Build.VERSION.SdkInt >= BuildVersionCodes.S) // API 31+
{
var manager = (VibratorManager?)ctx.GetSystemService(Context.VibratorManagerService);
return manager?.DefaultVibrator;
}
return (Vibrator?)ctx.GetSystemService(Context.VibratorService);
}
public void Impact(HapticImpact style)
{
var vibrator = GetVibrator();
if (vibrator?.HasVibrator != true) return;
if (Build.VERSION.SdkInt >= BuildVersionCodes.Q) // API 29+
{
var predefined = style switch
{
HapticImpact.Light => VibrationEffect.EffectTick,
HapticImpact.Medium => VibrationEffect.EffectClick,
HapticImpact.Heavy => VibrationEffect.EffectHeavyClick,
HapticImpact.Rigid => VibrationEffect.EffectDoubleClick,
_ => VibrationEffect.EffectClick
};
vibrator.Vibrate(VibrationEffect.CreatePredefined(predefined));
return;
}
if (Build.VERSION.SdkInt >= BuildVersionCodes.O) // API 26+
{
int amplitude = style switch
{
HapticImpact.Light => 60,
HapticImpact.Medium => 150,
HapticImpact.Heavy => 255,
_ => 150
};
vibrator.Vibrate(VibrationEffect.CreateOneShot(30, amplitude));
return;
}
#pragma warning disable CA1422
vibrator.Vibrate(30);
#pragma warning restore CA1422
}
public void PlayHeartbeat()
{
var vibrator = GetVibrator();
if (vibrator?.HasVibrator != true) return;
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
long[] timings = { 0, 30, 190, 40 };
int[] amplitudes = { 0, 180, 0, 220 };
vibrator.Vibrate(VibrationEffect.CreateWaveform(timings, amplitudes, -1));
}
}
}
Two Android-specific gotchas. First, VIBRATE must be declared in AndroidManifest.xml. It is a normal (install-time) permission, so no runtime prompt is needed, but forgetting it makes the vibrator silently no-op. Second, on Android 13+ the OS routes some haptics through the "Touch feedback" accessibility setting; users can disable haptics globally and your app has no way to check whether it is on.
If you need to enumerate multiple actuators (say, the linear main body vibrator plus a piezo tick in the shoulder buttons), call VibratorManager.VibratorIds and address each by ID. Only a handful of devices ship this hardware, but Samsung's Galaxy S25 and the Pixel 9 Pro Fold both expose secondary vibrators.
Building a shared IHapticService abstraction
The cross-platform surface I use is one interface and one enum per feedback family. Register the two platform implementations against the same interface using conditional compilation in MauiProgram.cs. This is the same pattern I recommend in my .NET MAUI platform-specific code guide, and it plays nicely with the MVVM Community Toolkit if you want haptics fired from a RelayCommand.
The no-op fallback is important. Windows and macOS Catalyst builds do not have haptic hardware, and letting the container throw an InvalidOperationException at resolve-time is worse than silently skipping the feedback. Ship a stub.
How do you add haptic feedback in .NET MAUI?
For the simplest case, a single tap on a button, inject IHapticService into your view model and call Impact(HapticImpact.Light) in the command handler. Because MAUI 10's DI container resolves per-view services if you register with AddTransient, keep the haptic service as a Singleton so the CHHapticEngine instance is reused across pages; spinning it up per call adds up to 40 ms of latency and burns a few milliamps each time.
public partial class LoginViewModel : ObservableObject
{
private readonly IHapticService _haptics;
public LoginViewModel(IHapticService haptics) => _haptics = haptics;
[RelayCommand]
private async Task SignInAsync()
{
var result = await _authService.SignInAsync();
if (result.IsSuccess)
_haptics.Notify(HapticNotification.Success);
else
_haptics.Notify(HapticNotification.Error);
}
}
For UI events that don't naturally hit a view model (a SwipeView commit, a CollectionView item drop, a pull-to-refresh threshold), hook the event and call the service from the code-behind. Honestly, don't put haptics inside a Behavior or attached property that fires on every property change; you'll trigger dozens of calls per second and the OS throttles the Taptic Engine, dropping subsequent events silently.
What is the difference between vibration and haptic feedback?
"Vibration" historically meant a coarse motor spinning an off-centre mass. That's the classic phone buzz. "Haptic feedback" means precise, short-duration signals from a linear resonant actuator (LRA) or piezo element that can start and stop within milliseconds. iPhones have used the Taptic Engine (an LRA) since the iPhone 6s; Pixel phones since Pixel 3 and most modern Samsung flagships also ship LRAs. The distinction matters because coarse vibration cannot render the "click" texture you feel from a mechanical keyboard, but an LRA can.
In the Android APIs, this is why EFFECT_TICK exists as a predefined constant: it tells the OS "give me a crisp haptic if you have an LRA, otherwise a short buzz." On devices without an LRA, the predefined effects fall back to a short amplitude burst that feels closer to old-school vibration. On iOS the distinction is enforced by the SupportsHaptics property. If it returns false, don't bother queuing CoreHaptics events. Design your patterns for LRAs and let the fallback be silent, not embarrassing.
Does haptic feedback drain battery?
Yes, but less than most developers assume. A single Taptic Engine transient consumes on the order of 3–5 mJ; a 500 ms continuous CoreHaptics event burns roughly 40 mJ. In practical terms, an app firing 30 haptic events per minute costs under 0.1% of an iPhone 15 Pro battery per hour. The bigger cost is CPU: waking the audio server for CoreHaptics or the vibrator service on Android adds context switches. If you fire haptics in a scrolling CollectionView, debounce to at most one event per 40 ms. That ceiling matches the human perceptual limit for discrete haptic pulses documented in the Apple Human Interface Guidelines for haptics.
On Android, unnecessary haptics also trigger the Doze-mode wake heuristics. Google's power team flagged this in a 2024 developer bulletin: apps that vibrate every 200 ms in the background are marked as "aggressive" and can be background-restricted. Keep haptics tied to user-visible interactions and you won't trip the heuristic.
Accessibility, testing, and simulator limits
Haptics interact with accessibility in two directions. First, users with hearing impairments often rely on haptics as an alternative signal; a well-designed error haptic on a form field paired with a red border is more accessible than the border alone. Second, some users find haptics uncomfortable or disorienting. iOS's "Reduce Motion" setting does not disable haptics but "Haptic Touch → Off" does. Your app should mirror the system setting through UIAccessibility.IsReduceMotionEnabled as an approximate proxy, or better, expose an in-app toggle.
Testing haptics is painful. The iOS Simulator doesn't fire the Taptic Engine at all; SupportsHaptics returns false and every event is a no-op. Android emulators fire vibrations that show up in the extended controls panel as a log line, but you feel nothing. You have to test on physical devices, and to catch cross-vendor differences you should have at least a Pixel, a Samsung, and a mid-range OEM (I use a Xiaomi Redmi Note as my worst-case) in the test lab.
For automated regression, wrap the haptic service in a decorator that records calls, and assert on the recorded log in xUnit tests. Don't try to unit test the platform implementations themselves; mocking CHHapticEngine or Vibrator adds no signal. If you're already following the setup in my .NET MAUI testing guide, the decorator pattern drops straight in.
Common failure modes I've hit in production
Silent on iPad. Every iPad model as of 2026 lacks a Taptic Engine. Guard with SupportsHaptics or ship an audio fallback.
Ghost buzz on Android 14 low-power mode. Some OEMs disable amplitude control below 20% battery. Test at low battery.
Missing VIBRATE permission. The manifest merger sometimes drops it if a dependency library declares tools:node="remove". Verify with aapt dump permissions.
Deprecated Vibrator warning at build time. Xamarin.Essentials-era code still calls Context.GetSystemService(VibratorService). Migrate to VibratorManager for API 31+ or you'll ship a warning that Play Console flags.
Frequently Asked Questions
What is haptic feedback in mobile apps?
Haptic feedback is a short, precise physical sensation the phone delivers through a linear actuator to confirm an action, such as a tap, a scroll landing, or an error. Unlike coarse vibration, it can render textures like a mechanical click or a soft thump within a few milliseconds.
Can iOS custom vibration patterns be created?
Yes. Use CoreHaptics (iOS 13+) with CHHapticEngine and CHHapticPattern, or load an AHAP JSON file bundled in your app. The high-level UIImpactFeedbackGenerator classes cover common cases but cannot express custom timing or intensity curves.
Why is haptic feedback not working on my Android device?
Common causes: the VIBRATE permission is missing from the manifest, the user disabled "Touch feedback" in Sound & Vibration settings, the device lacks a linear resonant actuator (many tablets and budget phones), or the app is running under low-power mode which suppresses non-critical haptics.
Does the .NET MAUI Essentials HapticFeedback API support custom patterns?
No. Microsoft.Maui.Devices.HapticFeedback.Perform() only accepts Click and LongPress. For custom patterns you must call platform-native APIs (CoreHaptics on iOS and VibrationEffect on Android) through a shared abstraction registered in the DI container.
Do I need runtime permission requests to use haptics on Android?
No. The VIBRATE permission is a normal install-time permission and doesn't need to be requested at runtime. You must still declare it in AndroidManifest.xml. For details on the runtime-vs-normal split, see the .NET MAUI runtime permissions guide.
Ship iOS Quick Actions and Android app shortcuts from one .NET MAUI 10 codebase, with static, dynamic, and pinned shortcuts, routing, and localization.
A working developer guide to CommunityToolkit MediaElement in .NET MAUI 10: install, XAML setup, HLS/DASH streaming, background audio, PiP, and fixes for the errors you will actually hit.
Add passkeys to .NET MAUI 10 using iOS AuthenticationServices and Android Credential Manager. Working C# handlers, association files, and a Fido2NetLib backend.