← Back to posts Cover image for Mastering Flutter Haptics: Beyond Basic Vibrations for Rich User Feedback

Mastering Flutter Haptics: Beyond Basic Vibrations for Rich User Feedback

· 4 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Haptics are a powerful but often overlooked tool in mobile app development. That subtle tap or gentle buzz can transform a flat interface into something that feels alive and responsive. In Flutter, we have the built-in HapticFeedback class, which seems like the obvious starting point. You’ve probably used it:

import 'package:flutter/services.dart';

ElevatedButton(
  onPressed: () {
    HapticFeedback.lightImpact();
    // Your action logic
  },
  child: Text('Tap Me'),
)

This works—sometimes. The problem many developers quickly encounter is inconsistency. HapticFeedback makes system-level calls, and the result depends entirely on how the device manufacturer has implemented (or not implemented) the underlying haptic engine. A heavyImpact() might feel satisfyingly crisp on a recent iPhone, but be a weak buzz, a different pattern, or completely silent on an older or lower-spec Android device. This silent failure is particularly frustrating; users just miss out on feedback without you knowing.

The core issue is one of control, or lack thereof. The standard API offers a limited, abstracted set of presets (lightImpact, mediumImpact, selectionClick). For truly rich tactile feedback—simulating a mechanical switch, providing directional cues in a game, or confirming a financial transaction—we need finer control over duration, intensity, and pattern.

Taking Control with the vibration Package

When you need reliable, basic vibration across platforms, the vibration package is a robust step up. It gives you direct control over vibration duration and can handle more complex patterns. Crucially, it provides feedback on capability via Vibration.hasVibrator(), allowing you to gracefully degrade your UX.

import 'package:vibration/vibration.dart';

// Check for capability
if (await Vibration.hasVibrator()) {
  // A simple 500ms vibration
  Vibration.vibrate(duration: 500);

  // A pattern: vibrate for 200ms, pause for 100ms, vibrate for 400ms
  Vibration.vibrate(pattern: [200, 100, 400]);
}

This is great for explicit notifications or alarms. However, for nuanced UI feedback, simple duration patterns can still feel crude compared to the precise “ticks” and “impacts” modern haptic hardware is capable of.

The Next Level: pulsar_haptics

For advanced, cross-platform haptic design that aims for consistency, the pulsar_haptics package is a game-changer. It introduces the concept of transients (sharp, sudden impulses) and continuous haptics (sustained textures), modeled after native iOS Core Haptics and Android VibrationEffect APIs.

It allows you to design a haptic experience in code with remarkable precision:

import 'package:pulsar_haptics/pulsar_haptics.dart';

final haptics = PulsarHaptics();

// A sharp, quick tap
await haptics.transientHaptic(
  intensity: 0.8, // 0.0 to 1.0
  sharpness: 0.9, // 0.0 to 1.0
);

// A sustained, textured rumble
await haptics.continuousHaptic(
  intensity: 0.6,
  sharpness: 0.3,
  duration: Duration(milliseconds: 750),
);

// A custom pattern: a sharp click followed by a decaying rumble
await haptics.customPatternHaptic([
  TransientEvent(intensity: 1.0, sharpness: 1.0),
  ContinuousEvent(
    intensity: 0.7,
    sharpness: 0.4,
    relativeTime: Duration(milliseconds: 50), // Start 50ms after the transient
    duration: Duration(milliseconds: 300),
  ),
]);

By using intensity and sharpness (concepts common to both major platforms), pulsar_haptics does a better job of translating your design intent across different devices. The package internally maps these parameters to the best-available native APIs.

Best Practices for Meaningful Haptics

Throwing haptics on every interaction is a sure way to annoy users. Here’s how to integrate them effectively:

  1. Design with Purpose: Tie haptics to meaningful user actions—successful form submission, reaching a list end, toggling a major setting, or placing a game piece. Avoid using them for trivial events.
  2. Provide a User Setting: Always include an option to disable haptics in your app’s settings. This is essential for accessibility and user preference.
  3. Graceful Degradation: Check for haptic capability (Vibration.hasVibrator or PulsarHaptics().isAvailable). If it’s not available, ensure your UI provides clear visual or auditory feedback as a fallback.
  4. Keep it Subtle: Haptics should enhance, not dominate. In most UI contexts, lower intensities (0.3-0.7) are more than sufficient.
  5. Test Extensively: The only way to understand the experience is to test on a range of physical devices. How does your sharpness: 0.9 feel on a Pixel versus a Samsung versus an iPhone?

Here’s a practical widget combining these principles:

class HapticFeedbackButton extends StatelessWidget {
  final VoidCallback onPressed;
  final Widget child;
  final bool importantAction;

  Future<void> _triggerHaptic() async {
    final haptics = PulsarHaptics();
    if (await haptics.isAvailable) {
      await haptics.transientHaptic(
        intensity: importantAction ? 0.8 : 0.5,
        sharpness: importantAction ? 0.7 : 0.9,
      );
    } else if (await Vibration.hasVibrator()) {
      // Fallback to basic vibration
      Vibration.vibrate(duration: importantAction ? 50 : 25);
    }
    // If no vibration is available, the visual feedback must carry the weight.
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        await _triggerHaptic();
        onPressed();
      },
      child: child,
    );
  }
}

By moving beyond HapticFeedback, you gain the tools to design tactile experiences that are consistent, intentional, and significantly enrich your app’s feel. Start by defining the emotional and functional role of haptics in your app, then use packages like vibration and pulsar_haptics to implement them with precision and cross-device respect.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.

Cover image for Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.