Open sourceFlutter package
Haptic feedback, vibration and animated widgets for Flutter - the right haptic at the right moment.
- MIT
- Android · iOS
- 9 animated widgets
DocsAPI reference
API reference
The full public API: Haptics, Vibration, HapticPattern, presets, capability detection, exceptions and widget signatures.
On this page8 sections
Library exports
A single import brings in everything: six primitive classes, three enums, a sealed exception hierarchy, two amplitude constants and nine animated widgets. Signatures below are taken verbatim from the package source.
import 'package:haptic_kit/haptic_kit.dart';| Symbol | Kind | Summary |
|---|---|---|
| Haptics | class | Short, semantic taps: impact, notification, selection, prepare. |
| Vibration | class | Longer vibrations: one-shot, custom waveforms, predefined effects, cancel. |
| HapticPattern | class | Fluent builder for Core Haptics / amplitude event sequences. |
| HapticEvent | class | A single event (intensity + sharpness) inside a HapticPattern. |
| VibrationPatterns | class | Ready-made presets - heartbeat, alarm, success, failure, ... |
| HapticCapabilities | class | Runtime snapshot of what the current device can do. |
| HapticImpactStyle | enum | Impact tap styles: light, medium, heavy, soft, rigid. |
| HapticNotificationStyle | enum | Notification styles: success, warning, error. |
| PredefinedEffect | enum | OS predefined effects: tick, click, doubleClick, heavyClick. |
| VibrationException | sealed class | Base class for every error thrown by the package. |
| UnsupportedHapticException | class | Device cannot render the requested capability. |
| InvalidVibrationArgumentException | class | A parameter is out of range or invalid. |
| PlatformVibrationException | class | Native call failed or the plugin is not registered. |
| kMaxAmplitude | const int | Maximum amplitude value (255). |
| kMinAmplitude | const int | Minimum perceptible amplitude (1). |
| HapticBounce | widget | Tap → squash → recoil → elastic settle with impact haptics. |
| HapticPulse | widget | Looping breathing scale pulse that ticks on every beat. |
| HapticRating | widget | Star rating with a cascading fill + a tick per star. |
| HapticShake | widget | Externally-triggered error wiggle. |
| HapticSlider | widget | Slider that ticks on each detent crossing. |
| HapticStepper | widget | −/+ counter with bouncing buttons. |
| HapticToggle | widget | Animated switch with a selection tick on flip. |
| PressAndHoldToConfirm | widget | Long-press with a progress ring + densifying ticks. |
| SlideToConfirm | widget | Drag the handle to the end to confirm. |
Haptics
Short, semantic haptic taps tied to UI events, exposed as static methods on Haptics. Named Haptics (not HapticFeedback) to avoid clashing with Flutter's own HapticFeedback from package:flutter/services.dart.
static Future<void> impact(HapticImpactStyle style)
static Future<void> notification(HapticNotificationStyle style)
static Future<void> selection()
static Future<bool> prepare()Returns: impact, notification and selection return Future<void>. prepare() returns Future<bool> - true when the platform actually pre-warmed something (iOS), false when the call is a no-op (Android).
await Haptics.impact(HapticImpactStyle.medium);
await Haptics.notification(HapticNotificationStyle.success);
await Haptics.selection();
// Optional: pre-warm the generators for the lowest-latency next tap.
final warmed = await Haptics.prepare(); // true on iOS, false on AndroidHapticImpactStyle
enum HapticImpactStyle {
light, // iOS: .light Android: EFFECT_TICK
medium, // iOS: .medium Android: EFFECT_CLICK
heavy, // iOS: .heavy Android: EFFECT_HEAVY_CLICK
soft, // iOS 13+, falls back to light
rigid, // iOS 13+, falls back to heavy
}HapticNotificationStyle
enum HapticNotificationStyle {
success, // Confirms a successful action
warning, // Warns about a non-fatal issue
error, // Signals a failure
}Vibration
Longer-form vibration on Vibration - one-shot durations, custom waveforms, predefined OS effects and cancellation. All methods are static.
/// Matches Android's VibrationEffect.MAX_AMPLITUDE.
const int kMaxAmplitude = 255;
/// Minimum amplitude that still produces a perceptible vibration.
const int kMinAmplitude = 1;Vibration.vibrate
Vibrate for duration. Amplitude is silently clamped on devices without amplitude control.
static Future<void> vibrate({
required Duration duration,
int? amplitude,
})| Parameter | Type | Req. | Default | Description |
|---|---|---|---|---|
| duration | Duration | Yes | - | How long to vibrate. Must be > 0 ms. |
| amplitude | int? | No | null | Strength in 1..255. Silently clamped on devices without amplitude control. null = device default. |
Returns: Future<void>.
Throws: InvalidVibrationArgumentException when duration is zero or negative, or when amplitude is set outside [1, 255].
Vibration.vibrateWaveform
Play a custom waveform. On iOS without Core Haptics support the call falls back to a sequence of system vibrations approximating timings.
static Future<void> vibrateWaveform({
required List<Duration> timings,
List<int>? amplitudes,
int repeat = -1,
})| Parameter | Type | Req. | Default | Description |
|---|---|---|---|---|
| timings | List<Duration> | Yes | - | Off/on durations, starting with an off period (Android createWaveform convention). Must not be empty. |
| amplitudes | List<int>? | No | null | Same length as timings, values in [0, 255] (0 = pause). null = default strength. |
| repeat | int | No | -1 | Index in timings to loop from, or -1 for no loop. |
Returns: Future<void>.
Throws: InvalidVibrationArgumentException when timings is empty, when amplitudes length differs from timings or holds a value outside [0, 255], or when repeat is neither -1 nor a valid index.
// Three pulses with growing amplitude.
await Vibration.vibrateWaveform(
timings: const [
Duration.zero,
Duration(milliseconds: 100),
Duration(milliseconds: 100),
Duration(milliseconds: 100),
Duration(milliseconds: 100),
Duration(milliseconds: 100),
],
amplitudes: const [0, 80, 0, 160, 0, 255],
);Vibration.playPredefined
Trigger one of the platform's predefined effects. Android API 29+ uses VibrationEffect.createPredefined; older Android and iOS translate to the closest equivalent.
static Future<void> playPredefined(PredefinedEffect effect)Returns: Future<void>.
enum PredefinedEffect {
tick, // Android: EFFECT_TICK
click, // Android: EFFECT_CLICK
doubleClick, // Android: EFFECT_DOUBLE_CLICK
heavyClick, // Android: EFFECT_HEAVY_CLICK
}Vibration.cancel
Stop any vibration started by this plugin.
static Future<void> cancel()Returns: Future<void>.
HapticPattern builder
A fluent builder for custom event sequences - CHHapticPattern on iOS, a createWaveform on Android where intensity maps to amplitude and sharpness is ignored. Each builder instance holds its own state and is single-use.
HapticEvent
A single event in a pattern.
const HapticEvent({
required double intensity, // 0.0..1.0 - overall strength
required double sharpness, // 0.0..1.0 - perceptual sharpness (iOS only)
Duration duration = Duration.zero, // non-zero = continuous event
Duration relativeTime = Duration.zero, // offset from pattern start
})| Parameter | Type | Req. | Default | Description |
|---|---|---|---|---|
| intensity | double | Yes | - | 0.0-1.0 - overall strength of the event. |
| sharpness | double | Yes | - | 0.0-1.0 - perceptual sharpness (iOS only; ignored on Android, which has no equivalent). |
| duration | Duration | No | Duration.zero | If non-zero, the event is continuous for this duration; otherwise it is a single tap. |
| relativeTime | Duration | No | Duration.zero | Offset from pattern start at which this event begins. |
Throws: An assertion error when intensity or sharpness is outside [0, 1].
HapticPattern.builder
Start a new pattern, then chain tap, continuous, pause and addEvent before calling play.
factory HapticPattern.builder()
HapticPattern tap({
required double intensity,
required double sharpness,
})
HapticPattern continuous({
required Duration duration,
required double intensity,
required double sharpness,
})
HapticPattern pause(Duration duration)
HapticPattern addEvent(HapticEvent event)
int get length // Number of events queued so far.
List<HapticEvent> get events // Snapshot of the current event list.
Future<void> play()Returns: tap, continuous, pause and addEvent return the same HapticPattern for chaining; play() returns Future<void>.
Throws: InvalidVibrationArgumentException from continuous/pause when the duration is <= 0 ms, and from play when the pattern has no events. play may also throw UnsupportedHapticException on devices with neither Core Haptics nor a waveform-amplitude vibrator.
await HapticPattern.builder()
.tap(intensity: 0.4, sharpness: 0.6)
.pause(const Duration(milliseconds: 80))
.tap(intensity: 1.0, sharpness: 0.9)
.continuous(
duration: const Duration(milliseconds: 250),
intensity: 0.7,
sharpness: 0.3,
)
.play();VibrationPatterns presets
Ready-to-use waveforms and haptic patterns on VibrationPatterns. Each entry returns a Future<void> so you can await playback in UI callbacks.
static Future<void> heartbeat()
static Future<void> notification()
static Future<void> alarm({bool repeat = true})
static Future<void> tick()
static Future<void> doubleTap()
static Future<void> success()
static Future<void> failure()
static Future<void> chargeUp({
Duration duration = const Duration(milliseconds: 600),
})| Preset | What it feels like |
|---|---|
| heartbeat() | Two short pulses mimicking a heartbeat. |
| notification() | Three escalating pulses - typical incoming-notification pattern. |
| alarm({bool repeat = true}) | Long ringing pattern - alarms / incoming calls. Loops by default. |
| tick() | Single very short tick - a hair stronger than a selection tap. |
| doubleTap() | Two quick, equal taps - a double-tap acknowledgement. |
| success() | Confirmation: light tap → strong tap. |
| failure() | Failure / cancel: two strong taps. |
| chargeUp({Duration duration = const Duration(milliseconds: 600)}) | Long ramp from soft to strong - a "charging up" moment. |
Capability detection
Query what the current device can do before reaching for an advanced capability. Call HapticCapabilities.query() once on app start (or the first user interaction) and cache the result - the values do not change at runtime.
static Future<HapticCapabilities> query()Returns: Future<HapticCapabilities> - a snapshot with the following fields.
| Parameter | Type | Req. | Default | Description |
|---|---|---|---|---|
| hasVibrator | bool | Device has any kind of vibrator hardware. | ||
| hasAmplitudeControl | bool | Device can render variable-amplitude waveforms. Android: Vibrator.hasAmplitudeControl() (API 26+); iOS: true when Core Haptics is available. | ||
| supportsCustomPatterns | bool | Custom HapticPatterns with intensity + sharpness. Android: API 26+ with amplitude control; iOS: Core Haptics devices (iPhone 8+). | ||
| supportsPredefinedEffects | bool | VibrationEffect.createPredefined (Android API 29+). Always false on iOS - predefined calls map to the closest impact style instead. | ||
| supportsImpactFeedback | bool | UIImpactFeedbackGenerator-style taps. iOS: true on iOS 10+; Android: true on API 23+ even without an amplitude-controlled vibrator. |
final caps = await HapticCapabilities.query();
if (caps.supportsCustomPatterns) {
await VibrationPatterns.success();
} else {
await Haptics.notification(HapticNotificationStyle.success);
}Exceptions
Every error thrown by haptic_kit is a subclass of the sealed VibrationException, so an exhaustive switch can handle them all.
sealed class VibrationException implements Exception {
const VibrationException(this.message);
final String message;
}
class UnsupportedHapticException extends VibrationException {
const UnsupportedHapticException(super.message);
}
class InvalidVibrationArgumentException extends VibrationException {
const InvalidVibrationArgumentException(super.message);
}
class PlatformVibrationException extends VibrationException {
const PlatformVibrationException(super.message, {this.code});
final String? code;
}| Exception | Thrown when |
|---|---|
| InvalidVibrationArgumentException | A parameter is out of range (negative duration, amplitude > 255, mismatched lists, ...). |
| UnsupportedHapticException | The device cannot render the requested capability. |
| PlatformVibrationException | The native side returned an error or the plugin is not registered. |
Animated widgets
Nine production-ready widgets that pair an animation with the matching haptic. Constructor signatures are shown below; for the full parameter tables, defaults and examples see the Animated widgets page. Widgets exposing a State class are driven externally via a GlobalKey.
HapticBounce
Tap wrapper that squashes, recoils past its resting size and settles with an elastic wobble. Fires impact(light) on press-down and medium on release.
const HapticBounce({
super.key,
required this.child,
this.onTap,
this.bounceOnRelease = true,
this.pressedScale = 0.92,
this.overshootScale = 1.12,
this.pressDuration = const Duration(milliseconds: 110),
this.releaseDuration = const Duration(milliseconds: 480),
this.haptics = true,
this.behavior = HitTestBehavior.opaque,
})PressAndHoldToConfirm
Hold to confirm with a progress ring and a 12-tick schedule that escalates selection → light → medium → heavy.
const PressAndHoldToConfirm({
super.key,
required this.child,
required this.onConfirm,
this.holdDuration = const Duration(seconds: 2),
this.ringSize = 96,
this.ringStrokeWidth = 6,
this.ringColor,
this.showRingAtFingerPosition = true,
})State (via GlobalKey<PressAndHoldToConfirmState>): void reset() re-arms the widget after a confirmation.
HapticToggle
Custom-drawn switch with a spring-back animation and a selection tick on flip. Controlled - the caller owns value.
const HapticToggle({
super.key,
required this.value,
required this.onChanged,
this.activeColor,
this.inactiveColor = const Color(0xFF3A3A3C),
this.thumbColor = Colors.white,
this.width = 56,
this.height = 32,
this.duration = const Duration(milliseconds: 220),
})HapticSlider
Slider that fires a haptic tick whenever the value crosses a discrete detent - like the iOS picker wheel.
const HapticSlider({
super.key,
required this.value,
required this.onChanged,
this.min = 0.0,
this.max = 1.0,
this.divisions,
this.tickEvery,
this.label,
this.activeColor,
this.tickStyle = HapticImpactStyle.light,
this.endTickStyle = HapticImpactStyle.medium,
})HapticStepper
−/+ counter with bouncing buttons and a number that slides. Each press fires light; hitting min/max fires heavy.
const HapticStepper({
super.key,
required this.value,
required this.onChanged,
this.min = 0,
this.max = 99,
this.step = 1,
})HapticShake
Externally-triggered horizontal wiggle + notification(error) - the classic "wrong password" shake.
const HapticShake({
super.key,
required this.child,
this.amplitude = 10.0,
this.duration = const Duration(milliseconds: 420),
this.haptics = true,
})State (via GlobalKey<HapticShakeState>): Future<void> shake() plays the shake once (restarts if already shaking).
HapticPulse
Looping "breathing" scale pulse that fires a haptic tap on every beat - the attention-getter counterpart to HapticShake.
const HapticPulse({
super.key,
required this.child,
this.minScale = 1.0,
this.maxScale = 1.08,
this.period = const Duration(milliseconds: 600),
this.autoPlay = true,
this.pulseCount,
this.impactStyle = HapticImpactStyle.light,
this.haptics = true,
})State (via GlobalKey<HapticPulseState>): bool get isPulsing, void start(), void stop().
SlideToConfirm
Drag the handle from edge to edge to confirm, with light ticks at 25% / 50% / 75% and a heavy impact at 100%.
const SlideToConfirm({
super.key,
required this.onConfirmed,
this.label = 'Slide to confirm',
this.height = 64,
this.handleIcon = Icons.arrow_forward,
this.confirmedIcon = Icons.check,
this.trackColor,
this.handleColor,
this.textColor,
})State (via GlobalKey<SlideToConfirmState>): void reset() re-arms the widget after a confirmation.
HapticRating
Star rating that fills with a cascading animation, firing one selection tick per star as it lights up.
const HapticRating({
super.key,
required this.value,
required this.onChanged,
this.starCount = 5,
this.size = 36,
this.spacing = 6,
this.activeColor = Colors.amber,
this.inactiveColor = const Color(0xFF3A3A3C),
this.cascadeDelay = const Duration(milliseconds: 65),
})Let's make something together.
No overengineering. No wasted time. Just straight-up execution.
Or book a call with Eryk (opens the booking calendar)[email protected]


