Flutter package
haptic_kit
Haptic feedback, vibration and animated widgets for Flutter — the right haptic at the right moment.
Animated widgets
Nine production-ready widgets that pair each animation with the correct haptic automatically.
source: README.mdWidget
HapticBounce
Wraps any child so a tap squashes it, recoils past its resting size and settles with an elastic wobble — a 3-segment TweenSequence with weights 1 : 2 : 3 (squash easeIn, recoil easeOutCubic, settle elasticOut). Fires Haptics.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,
})| Param | Type | Default | Meaning |
|---|---|---|---|
child | Widget | required | The widget to scale on press. |
onTap | VoidCallback? | — | Equivalent to GestureDetector.onTap. |
bounceOnRelease | bool | true | true: recoil + elastic-settle on release; false: symmetric return, no overshoot. |
pressedScale | double | 0.92 | Scale at the bottom of the press; must be in (0, 1). |
overshootScale | double | 1.12 | Peak scale during recoil; must be >= 1.0; ignored when bounceOnRelease is false. |
pressDuration | Duration | 110 ms | Press-down animation duration. |
releaseDuration | Duration | 480 ms | Release animation (recoil + elastic settle). |
haptics | bool | true | Fire Haptics.impact(light) on press-down and medium on release. |
behavior | HitTestBehavior | opaque | Passed to the underlying GestureDetector. |
Constraint violations throw an ArgumentError at construction time, in both debug and release.
HapticBounce(
onTap: () => doSomething(),
child: Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(/* ... */),
child: const Text('Press me'),
),
)Widget
PressAndHoldToConfirm
Hold-to-confirm with a progress ring and a densifying tick schedule. A single AnimationController drives the ring, the haptic schedule and the callback, so there are no race conditions. The 12 ticks escalate selection → light → medium → heavy and seal with a final heavy impact at completion.
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,
})| Param | Type | Default | Meaning |
|---|---|---|---|
child | Widget | required | The widget rendered under the finger. |
onConfirm | VoidCallback | required | Fired exactly once when the press completes; use reset() to allow another confirmation. |
holdDuration | Duration | 2 s | How long the user must hold. |
ringSize | double | 96 | Diameter of the progress ring in logical pixels. |
ringStrokeWidth | double | 6 | Stroke width of the ring. |
ringColor | Color? | Theme.colorScheme.primary | Ring colour. |
showRingAtFingerPosition | bool | true | true: ring follows the finger; false: centred over the child. |
The state method void reset() on PressAndHoldToConfirmState re-arms the widget after a successful confirmation. It has no effect while a press is in progress or before the first confirmation.
final key = GlobalKey<PressAndHoldToConfirmState>();
PressAndHoldToConfirm(
key: key,
holdDuration: const Duration(seconds: 2),
onConfirm: () => unbox(),
child: const SizedBox(
height: 240,
child: Center(child: Icon(Icons.card_giftcard, size: 96)),
),
)
// Re-arm for another confirmation later:
key.currentState?.reset();Widget
HapticToggle
A custom-drawn pill + thumb with a spring-back animation on every change. It behaves like a Switch (controlled — the caller owns value and handles changes through onChanged), fires Haptics.selection() on toggle, and slides with Curves.easeOutBack.
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),
})| Param | Type | Default |
|---|---|---|
value | bool | required |
onChanged | ValueChanged<bool>? | required |
activeColor | Color? | Theme.colorScheme.primary |
inactiveColor | Color | Color(0xFF3A3A3C) |
thumbColor | Color | Colors.white |
width | double | 56 |
height | double | 32 |
duration | Duration | 220 ms |
HapticToggle(
value: _enabled,
onChanged: (v) => setState(() => _enabled = v),
)Widget
HapticSlider
A slider that fires a haptic tick whenever the value crosses a discrete detent — like the iOS picker wheel. It caches the last detent index to detect crossings; intermediate ticks and the min/max endpoints get different impact styles.
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,
})| Param | Type | Default | Meaning |
|---|---|---|---|
value | double | required | — |
onChanged | ValueChanged<double> | required | — |
min / max | double | 0.0 / 1.0 | — |
divisions | int? | — | Discrete divisions; if both are set, divisions wins; must be > 0. |
tickEvery | double? | — | Distance between ticks in value units (used when divisions is null); must be > 0. |
label | String? | — | Passed to Slider. |
activeColor | Color? | — | Passed to Slider. |
tickStyle | HapticImpactStyle | light | Haptic style for intermediate ticks. |
endTickStyle | HapticImpactStyle | medium | Haptic style for the min and max endpoints. |
HapticSlider(
value: _v,
min: 0,
max: 100,
divisions: 10, // tick every 10 units
onChanged: (v) => setState(() => _v = v),
)Widget
HapticStepper
A numeric stepper with bouncing −/+ buttons and a number that slides up (on increment) or down (on decrement). Each button press fires a light haptic; hitting min or max fires a heavy haptic to signal the boundary. It composes HapticBounce buttons with an AnimatedSwitcher.
const HapticStepper({
super.key,
required this.value,
required this.onChanged,
this.min = 0,
this.max = 99,
this.step = 1,
})| Param | Type | Default | Constraint |
|---|---|---|---|
value | int | required | — |
onChanged | ValueChanged<int> | required | — |
min | int | 0 | min < max (assert) |
max | int | 99 | — |
step | int | 1 | step > 0 (assert) |
HapticStepper(
value: _count,
min: 0,
max: 99,
onChanged: (v) => setState(() => _count = v),
)Widget
HapticShake
Wiggles its child with a decaying horizontal shake and fires an error notification — the classic "wrong password" motion. It is triggered externally through a GlobalKey: a 6-segment TweenSequence of decreasing-amplitude translations ending at zero.
const HapticShake({
super.key,
required this.child,
this.amplitude = 10.0,
this.duration = const Duration(milliseconds: 420),
this.haptics = true,
})| Param | Type | Default | Meaning |
|---|---|---|---|
child | Widget | required | — |
amplitude | double | 10.0 | Maximum horizontal displacement in logical pixels. Subsequent swings decay from this value. |
duration | Duration | 420 ms | Total duration of the shake. |
haptics | bool | true | Fire Haptics.notification(error) at the start of the shake. |
The state method Future<void> shake() on HapticShakeState plays the shake animation once; calling it while already shaking restarts from the beginning.
final shakeKey = GlobalKey<HapticShakeState>();
HapticShake(key: shakeKey, child: TextField(/* ... */));
// On validation failure:
shakeKey.currentState?.shake();Widget
SlideToConfirm
Drag a handle to the end to confirm — the iconic pattern in payment, driver and "are you sure" flows. Light ticks fire at 25%, 50% and 75% progress; reaching 100% fires a heavy impact + onConfirmed and the handle stays at the end. Releasing before 100% springs the handle back with a light tick.
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,
})| Param | Type | Default |
|---|---|---|
onConfirmed | VoidCallback | required |
label | String | 'Slide to confirm' |
height | double | 64 |
handleIcon | IconData | Icons.arrow_forward |
confirmedIcon | IconData | Icons.check |
trackColor / handleColor / textColor | Color? | theme-derived |
The state method void reset() on SlideToConfirmState re-arms the widget after a successful confirmation.
SlideToConfirm(
label: 'Slide to pay',
onConfirmed: () => pay(),
)Widget
HapticRating
A star rating that cascades on tap: tapping the 4th star lights up four stars in sequence, firing one selection tick per star. The cascade is driven by a Timer.periodic with a 65 ms delay between stars.
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),
})| Param | Type | Default | Meaning |
|---|---|---|---|
value | int | required | Current rating, in 0..starCount. |
onChanged | ValueChanged<int> | required | — |
starCount | int | 5 | Must be > 0 (assert). |
size | double | 36 | Star icon size. |
spacing | double | 6 | Gap between stars. |
activeColor | Color | Colors.amber | — |
inactiveColor | Color | Color(0xFF3A3A3C) | — |
cascadeDelay | Duration | 65 ms | Time between each star lighting up during the cascade. |
HapticRating(
value: _rating,
starCount: 5,
onChanged: (v) => setState(() => _rating = v),
)Widget
HapticPulse
A repeating scale pulse (heartbeat) around its child, with a light haptic tick on each peak. Pulses forever by default or a fixed number of times via pulseCount; drive it manually with the state's start() / stop() and read isPulsing.
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,
})| Param | Type | Default | Meaning |
|---|---|---|---|
child | Widget | required | The widget to pulse. |
minScale | double | 1.0 | Scale at the trough of each pulse (must be > 0). |
maxScale | double | 1.08 | Scale at the peak of each pulse (must be > 0). |
period | Duration | 600 ms | Length of one full pulse cycle. |
autoPlay | bool | true | Start pulsing as soon as the widget mounts. |
pulseCount | int? | null | Number of pulses; null pulses infinitely (must be > 0 when set). |
impactStyle | HapticImpactStyle | light | Haptic fired at each pulse peak. |
haptics | bool | true | Set false to animate silently. |
HapticPulse(
maxScale: 1.12,
period: const Duration(milliseconds: 500),
pulseCount: 3, // null = pulse forever
impactStyle: HapticImpactStyle.light,
child: const Icon(Icons.favorite, size: 48),
)Let's make something together.
If you have any questions about a new project or other inquiries, feel free to contact us. We will get back to you as soon as possible.
