Controlling Touch Handling in Flutter with AbsorbPointer and IgnorePointer
AbsorbPointer and IgnorePointer both prevent widgets in their subtree from reacting to pointer input (tap, drag, scroll). The difference lies in how they partiicpate in hit testing:
- AbsorbPointer: removes its descendants from hit testing but is itself still hit. Encestors can receive the event; widgets behind it do not.
- IgnorePointer: removes both itself and its descendants from hit testing. Events pass through to widgets behind it.
AbsorbPointer
Disable a group of interactive widgets without changing each handler. By default, absorbing is true, so child widgets won’t receive input.
AbsorbPointer(
// true by default; set to false to re-enable input to children
absorbing: true,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () => debugPrint('Pressed A'),
child: const Text('A'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => debugPrint('Pressed B'),
child: const Text('B'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => debugPrint('Pressed C'),
child: const Text('C'),
),
],
),
)
Re-enable input by flipping the flag:
AbsorbPointer(
absorbing: false,
child: /* interactive subtree */
)
IgnorePointer
Similar usage, but the pointer is ignored for both the widget and its subtree; events hit widgets behind it.
IgnorePointer(
// true by default
ignoring: true,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () => debugPrint('Pressed X'),
child: const Text('X'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => debugPrint('Pressed Y'),
child: const Text('Y'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => debugPrint('Pressed Z'),
child: const Text('Z'),
),
],
),
)
Hit Testing Difference in Practice
Two stacked boxes: a 200×200 red box and a centered 100×100 blue box. Each has its own pointer listener.
return SizedBox(
height: 200,
width: 200,
child: Stack(
alignment: Alignment.center,
children: [
Listener(
onPointerDown: (_) => debugPrint('tap: RED'),
child: Container(color: Colors.red),
),
Listener(
onPointerDown: (_) => debugPrint('tap: BLUE'),
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
],
),
);
- Tapping the blue square printts:
tap: BLUE
- Tapping the red area outside the blue square prints:
tap: RED
Wrap blue with AbsorbPointer
The outer Listener surrounds AbsorbPointer; the inner Listener is inside it. The inner hendler will not fire, but the outer one still will.
return SizedBox(
height: 200,
width: 200,
child: Stack(
alignment: Alignment.center,
children: [
Listener(
onPointerDown: (_) => debugPrint('tap: RED'),
child: Container(color: Colors.red),
),
Listener(
onPointerDown: (_) => debugPrint('tap: BLUE (outer parent)'),
child: AbsorbPointer(
child: Listener(
onPointerDown: (_) => debugPrint('tap: BLUE (inner child)'),
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
),
],
),
);
- Tapping the blue square prints:
tap: BLUE (outer parent)
The AbsorbPointer itself participates in hit testing and consumes the event for its subtree; the ancestor Listener still receives it.
Replace with IgnorePointer
Change AbsorbPointer too IgnorePointer in the previous snippet (keeping the same structure):
IgnorePointer(
child: Listener(
onPointerDown: (_) => debugPrint('tap: BLUE (inner child)'),
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
)
- Tapping the blue square prints:
tap: RED
IgnorePointer removes itself and its subtree from hit testing, so the touch goes through to the red box behind it.
Typical Use Cases
- Temporarily disable or enable a set of controls (buttons, inputs, scrollables) with a single flag.
- Lock the entire screen or a section during a blocking operation (e.g., while a network request is in progress).
- Allow touches to pass through overlay UIs (e.g., transparent hints or tutorials) to underlying content using IgnorePointer.
- Maintain parent-level gesture handling while suppressing child interactions using AbsorbPointer.