← 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 Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.

Cover image for Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.

Cover image for Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.